Files
ts-mobile-go/go/teamspeak/bridge.go
T

1559 lines
42 KiB
Go
Raw Permalink Normal View History

2026-07-20 19:01:03 +08:00
// Package teamspeak 提供 gomobile 友好的 TeamSpeak 客户端 API。
//
// gomobile 限制:
// - 不支持 []string、[]*T 等复杂切片类型导出
// - 不支持 Go 的 error 类型,改用 string 返回错误
// - 回调必须通过 interface 定义
// - 复杂数据通过 JSON 字符串传递
package teamspeak
import (
"context"
"encoding/json"
"fmt"
"log"
"sync"
"time"
ts "github.com/honeybbq/teamspeak-go"
"github.com/honeybbq/teamspeak-go/crypto"
)
// queuedEvent 封装待处理的事件,用于 JNI 回调串行化
type queuedEvent struct {
evtType string
data any
}
// eventQueue owns one callback consumer. A new connection receives a fresh
// instance so a stopped consumer can never resume against a later session.
const maxQueuedPCMEvents = 12
type eventQueue struct {
mu sync.Mutex
cond *sync.Cond
events []queuedEvent
callback EventCallback
stopped bool
pcmCount int
stopOnce sync.Once
}
func newEventQueue(callback EventCallback) *eventQueue {
q := &eventQueue{callback: callback}
q.cond = sync.NewCond(&q.mu)
return q
}
// ─── 内部数据类型(不导出给 gomobile) ─────────────────────
// serverInfoJSON 用于 JSON 序列化的服务器信息
type serverInfoJSON struct {
Name string `json:"name"`
WelcomeMessage string `json:"welcomeMessage"`
MaxClients int `json:"maxClients"`
ClientsOnline int `json:"clientsOnline"`
ChannelsOnline int `json:"channelsOnline"`
Uptime string `json:"uptime"`
Version string `json:"version"`
Platform string `json:"platform"`
Created string `json:"created"`
IconID string `json:"iconId"`
DefaultServerGroup int `json:"defaultServerGroup"`
DefaultChannelGroup int `json:"defaultChannelGroup"`
}
// channelDetailedInfoJSON 用于 JSON 序列化的单频道详情(来自 channelinfo cid=X
type channelDetailedInfoJSON struct {
ID string `json:"id"`
ParentID string `json:"parentId"`
Name string `json:"name"`
Topic string `json:"topic"`
Description string `json:"description"`
Codec int `json:"codec"`
CodecQuality int `json:"codecQuality"`
MaxClients int `json:"maxClients"`
MaxFamilyClients int `json:"maxFamilyClients"`
NeededTalkPower int `json:"neededTalkPower"`
IconID string `json:"iconId"`
IsPermanent bool `json:"isPermanent"`
IsSemiPermanent bool `json:"isSemiPermanent"`
IsDefault bool `json:"isDefault"`
IsPassword bool `json:"isPassword"`
Order string `json:"order"`
BannerGfxURL string `json:"bannerGfxUrl"`
}
// clientDetailedInfoJSON 用于 JSON 序列化的单客户端详情(来自 clientinfo clid=X
type clientDetailedInfoJSON struct {
ID string `json:"id"`
Nickname string `json:"nickname"`
UID string `json:"uid"`
ChannelID string `json:"channelId"`
Type int `json:"type"`
ServerGroups []string `json:"serverGroups"`
Away bool `json:"away"`
AwayMessage string `json:"awayMessage"`
InputMuted bool `json:"inputMuted"`
OutputMuted bool `json:"outputMuted"`
Platform string `json:"platform"`
Version string `json:"version"`
IP string `json:"ip"`
Created string `json:"created"`
LastConnected string `json:"lastConnected"`
TotalConnections int `json:"totalConnections"`
Description string `json:"description"`
IconID string `json:"iconId"`
}
// dbClientJSON 用于 JSON 序列化的数据库客户端
type dbClientJSON struct {
DBID string `json:"dbid"`
UID string `json:"uid"`
Nickname string `json:"nickname"`
Created string `json:"created"`
LastConnected string `json:"lastConnected"`
TotalConnections int `json:"totalConnections"`
Description string `json:"description"`
}
// banEntryJSON 用于 JSON 序列化的 ban 条目
type banEntryJSON struct {
BanID string `json:"banId"`
IP string `json:"ip"`
Name string `json:"name"`
UID string `json:"uid"`
Created string `json:"created"`
InvokerName string `json:"invokerName"`
InvokerUID string `json:"invokerUid"`
Reason string `json:"reason"`
Enforcement bool `json:"enforcement"`
}
// fileEntryJSON 用于 JSON 序列化的文件条目
type fileEntryJSON struct {
Name string `json:"name"`
Size string `json:"size"`
DateTime string `json:"dateTime"`
IsFile bool `json:"isFile"`
}
// tokenEntryJSON 用于 JSON 序列化的 token 条目
type tokenEntryJSON struct {
Token string `json:"token"`
TokenType int `json:"tokenType"`
TokenID1 string `json:"tokenId1"`
TokenID2 string `json:"tokenId2"`
Created string `json:"created"`
Description string `json:"description"`
}
// complaintEntryJSON 用于 JSON 序列化的投诉条目
type complaintEntryJSON struct {
FromDBID string `json:"fromDbid"`
ToDBID string `json:"toDbid"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
}
// channelJSON 用于 JSON 序列化的频道信息
type channelJSON struct {
ID string `json:"id"`
Name string `json:"name"`
ParentID string `json:"parentId"`
Description string `json:"description"`
IsPassword bool `json:"isPassword"`
}
// channelDetailedJSON 用于 JSON 序列化的详细频道信息(含 flags、voice、limits、icon
type channelDetailedJSON struct {
ID string `json:"id"`
Name string `json:"name"`
ParentID string `json:"parentId"`
Topic string `json:"topic"`
Order string `json:"order"`
Codec int `json:"codec"`
CodecQuality int `json:"codecQuality"`
NeededTalkPower int `json:"neededTalkPower"`
MaxClients int `json:"maxClients"`
MaxFamilyClients int `json:"maxFamilyClients"`
IsMaxClientsUnlimited bool `json:"isMaxClientsUnlimited"`
IsMaxFamilyClientsUnlimited bool `json:"isMaxFamilyClientsUnlimited"`
IsPermanent bool `json:"isPermanent"`
IsSemiPermanent bool `json:"isSemiPermanent"`
IsDefault bool `json:"isDefault"`
IsPassword bool `json:"isPassword"`
IsOrdered bool `json:"isOrdered"`
IconID string `json:"iconId"`
NeededModifyPower int `json:"neededModifyPower"`
}
// clientJSON 用于 JSON 序列化的客户端信息
type clientJSON struct {
ID int `json:"id"`
Nickname string `json:"nickname"`
UID string `json:"uid"`
ChannelID string `json:"channelId"`
ServerGroups []string `json:"serverGroups"`
IsSelf bool `json:"isSelf"`
}
// ─── 数据类型(gomobile 可导出) ────────────────────────────
// Channel 频道信息(gomobile 导出)
type Channel struct {
ID string
Name string
ParentID string
Description string
}
// Client 客户端信息(gomobile 导出)
type Client struct {
ID int
Nickname string
UID string
ChannelID string
IsSelf bool
}
// TextMsg 文字消息(gomobile 导出)
type TextMsg struct {
InvokerName string
InvokerUID string
Message string
TargetMode int
TargetID string
}
// PokeEvent Poke 事件(gomobile 导出)
type PokeEvent struct {
InvokerID int
InvokerName string
InvokerUID string
Message string
}
// ─── 事件回调接口 ─────────────────────────────────────────
// EventCallback 事件回调接口,Android 端实现此接口接收事件
type EventCallback interface {
OnConnected()
OnDisconnected(message string)
OnTextMessage(msg *TextMsg)
OnClientEnter(client *Client)
OnClientLeave(id int, reasonMsg string)
OnClientMoved(id int, targetChannelID string)
OnKicked(reason string)
OnVoiceData(clientID int, data []byte, codec int, sequence int, isWhisper bool)
// OnMixedVoicePCM receives a 20 ms, 48 kHz interleaved stereo PCM16 little-endian frame.
OnMixedVoicePCM(pcm []byte)
OnClientSpeaking(clientID int, speaking bool)
OnPoked(event *PokeEvent)
}
// ─── TSClient 主类 ────────────────────────────────────────
// TSClient TeamSpeak 客户端封装
type TSClient struct {
mu sync.Mutex
client *ts.Client
callback EventCallback
connected bool
host string // 服务器地址(用于文件传输 TCP 连接)
// 事件队列:保证所有 JNI 回调在同一个协程中顺序执行
evtQueueMu sync.Mutex // protects eventQueue replacement
evtQueue *eventQueue
// receiveFactory is platform-owned and intentionally private so gomobile
// never exposes the native decoder implementation. Android supplies a
// libopus-backed factory; unsupported host builds leave it nil.
receiveFactory voiceDecoderFactory
receive *receiveAudio
}
// NewClient 创建新的 TeamSpeak 客户端
func NewClient() *TSClient {
factory := newPlatformVoiceDecoderFactory()
log.Printf("[TSVoice] platform decoder factory=%T available=%t", factory, factory != nil)
return &TSClient{receiveFactory: factory}
}
// Connect 连接到 TeamSpeak 服务器
// 返回空字符串表示成功,非空为错误信息
func (c *TSClient) Connect(host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string {
// Ensure repeated attempts on this wrapper cannot leave a prior transport alive.
c.Disconnect()
// A prior failed or disconnected session may have left its consumer waiting.
// Stop it before replacing the callback and starting this session's queue.
c.stopEventQueue()
c.mu.Lock()
c.callback = cb
c.connected = false
c.host = host
c.mu.Unlock()
c.initEventQueue()
c.ensureReceiveFactory()
c.StartReceiveAudio()
identity, err := crypto.GenerateIdentity(8)
if err != nil {
return fmt.Sprintf("生成身份失败: %v", err)
}
opts := []ts.ClientOption{}
if password != "" {
opts = append(opts, ts.WithServerPassword(password))
}
if defaultChannel != "" {
opts = append(opts, ts.WithDefaultChannel(defaultChannel))
}
if defaultChannelPassword != "" {
opts = append(opts, ts.WithDefaultChannelPassword(defaultChannelPassword))
}
client := ts.NewClient(identity, host, nickname, opts...)
c.mu.Lock()
c.client = client
c.mu.Unlock()
// 注册事件
c.registerEvents(client)
if err := client.Connect(); err != nil {
c.Disconnect()
return fmt.Sprintf("连接失败: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := client.WaitConnected(ctx); err != nil {
c.Disconnect()
return fmt.Sprintf("连接失败: %v", err)
}
return ""
}
// Disconnect 断开连接
func (c *TSClient) Disconnect() {
c.StopReceiveAudio()
// 停止事件消费协程
c.stopEventQueue()
c.mu.Lock()
client := c.client
c.client = nil
c.connected = false
c.mu.Unlock()
if client != nil {
if err := client.Disconnect(); err != nil {
log.Printf("[TSBridge] disconnect error: %v", err)
}
}
}
// IsConnected 是否已连接
func (c *TSClient) IsConnected() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.connected && c.client != nil
}
// GetChannelsJSON 获取频道列表(JSON 格式)
// gomobile 不支持返回 []*Channel,所以用 JSON 字符串传递
func (c *TSClient) GetChannelsJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
log.Printf("[TSBridge] GetChannelsJSON: client is nil")
return "[]"
}
log.Printf("[TSBridge] GetChannelsJSON: calling ListChannelsDetailed...")
channels, err := client.ListChannelsDetailed()
if err != nil {
log.Printf("[TSBridge] ListChannelsDetailed error: %v", err)
return "[]"
}
log.Printf("[TSBridge] ListChannelsDetailed returned %d channels", len(channels))
result := make([]channelJSON, len(channels))
for i, ch := range channels {
parentID := "0"
if ch.ParentID != 0 {
parentID = fmt.Sprintf("%d", ch.ParentID)
}
result[i] = channelJSON{
ID: fmt.Sprintf("%d", ch.ID),
Name: ch.Name,
ParentID: parentID,
IsPassword: ch.IsPassword,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// GetChannelsDetailedJSON 获取详细频道列表(JSON 格式)
// 通过 channellist -topic -flags -voice -limits -icon 获取更丰富的频道属性,
// 包括:密码标志、编解码器、人数限制、永久/半永久标志、图标 ID 等。
// gomobile 不支持返回 []*ChannelInfoDetailed,所以用 JSON 字符串传递。
func (c *TSClient) GetChannelsDetailedJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
channels, err := client.ListChannelsDetailed()
if err != nil {
log.Printf("[TSBridge] ListChannelsDetailed error: %v", err)
return "[]"
}
result := make([]channelDetailedJSON, len(channels))
for i, ch := range channels {
parentID := "0"
if ch.ParentID != 0 {
parentID = fmt.Sprintf("%d", ch.ParentID)
}
result[i] = channelDetailedJSON{
ID: fmt.Sprintf("%d", ch.ID),
Name: ch.Name,
ParentID: parentID,
Topic: ch.Topic,
Order: fmt.Sprintf("%d", ch.Order),
Codec: ch.Codec,
CodecQuality: ch.CodecQuality,
NeededTalkPower: ch.NeededTalkPower,
MaxClients: ch.MaxClients,
MaxFamilyClients: ch.MaxFamilyClients,
IsMaxClientsUnlimited: ch.IsMaxClientsUnlimited,
IsMaxFamilyClientsUnlimited: ch.IsMaxFamilyClientsUnlimited,
IsPermanent: ch.IsPermanent,
IsSemiPermanent: ch.IsSemiPermanent,
IsDefault: ch.IsDefault,
IsPassword: ch.IsPassword,
IsOrdered: ch.Order > 0,
IconID: fmt.Sprintf("%d", ch.IconID),
NeededModifyPower: ch.NeededModifyPower,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// GetClientsJSON 获取在线客户端列表(JSON 格式)
// gomobile 不支持返回 []*Client,所以用 JSON 字符串传递
func (c *TSClient) GetClientsJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
clients, err := client.ListClients()
if err != nil {
log.Printf("[TSBridge] ListClients error: %v", err)
return "[]"
}
myID := client.ClientID()
result := make([]clientJSON, len(clients))
for i, cl := range clients {
result[i] = clientJSON{
ID: int(cl.ID),
Nickname: cl.Nickname,
UID: cl.UID,
ChannelID: fmt.Sprintf("%d", cl.ChannelID),
ServerGroups: cl.ServerGroups,
IsSelf: cl.ID == myID,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// ─── 服务器信息 ────────────────────────────────────────────
// GetServerInfoJSON 获取服务器信息(JSON 格式)
// gomobile 不支持返回 *ServerInfo,所以用 JSON 字符串传递
func (c *TSClient) GetServerInfoJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "{}"
}
info, err := client.GetServerInfo()
if err != nil {
log.Printf("[TSBridge] GetServerInfo error: %v", err)
return "{}"
}
result := serverInfoJSON{
Name: info.Name,
WelcomeMessage: info.WelcomeMessage,
MaxClients: info.MaxClients,
ClientsOnline: info.ClientsOnline,
ChannelsOnline: info.ChannelsOnline,
Uptime: fmt.Sprintf("%d", info.Uptime),
Version: info.Version,
Platform: info.Platform,
Created: fmt.Sprintf("%d", info.Created),
IconID: fmt.Sprintf("%d", info.IconID),
DefaultServerGroup: info.DefaultServerGroup,
DefaultChannelGroup: info.DefaultChannelGroup,
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "{}"
}
return string(data)
}
// ─── 频道详情 ──────────────────────────────────────────────
// GetChannelDetailInfoJSON 获取单个频道的详细信息(JSON 格式)
// 通过 channelinfo cid=X 获取完整的频道属性(含 description)。
// 返回空字符串表示错误。
func (c *TSClient) GetChannelDetailInfoJSON(channelIDStr string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "{}"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return "{}"
}
info, err := client.GetChannelInfo(channelID)
if err != nil {
log.Printf("[TSBridge] GetChannelInfo error: %v", err)
return "{}"
}
result := channelDetailedInfoJSON{
ID: fmt.Sprintf("%d", info.ID),
ParentID: fmt.Sprintf("%d", info.ParentID),
Name: info.Name,
Topic: info.Topic,
Description: info.Description,
Codec: info.Codec,
CodecQuality: info.CodecQuality,
MaxClients: info.MaxClients,
MaxFamilyClients: info.MaxFamilyClients,
NeededTalkPower: info.NeededTalkPower,
IconID: fmt.Sprintf("%d", info.IconID),
IsPermanent: info.IsPermanent,
IsSemiPermanent: info.IsSemiPermanent,
IsDefault: info.IsDefault,
IsPassword: info.IsPassword,
Order: fmt.Sprintf("%d", info.Order),
BannerGfxURL: info.BannerGfxURL,
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "{}"
}
return string(data)
}
// ─── 客户端详情 ────────────────────────────────────────────
// GetClientDetailInfoJSON 获取单个客户端的详细信息(JSON 格式)
// 通过 clientinfo clid=X 获取完整的客户端属性。
func (c *TSClient) GetClientDetailInfoJSON(clid int) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "{}"
}
info, err := client.GetClientDetailInfo(uint16(clid))
if err != nil {
log.Printf("[TSBridge] GetClientDetailInfo error: %v", err)
return "{}"
}
result := clientDetailedInfoJSON{
ID: fmt.Sprintf("%d", info.ID),
Nickname: info.Nickname,
UID: info.UID,
ChannelID: fmt.Sprintf("%d", info.ChannelID),
Type: info.Type,
ServerGroups: info.ServerGroups,
Away: info.Away,
AwayMessage: info.AwayMessage,
InputMuted: info.InputMuted,
OutputMuted: info.OutputMuted,
Platform: info.Platform,
Version: info.Version,
IP: info.IP,
Created: fmt.Sprintf("%d", info.Created),
LastConnected: fmt.Sprintf("%d", info.LastConnected),
TotalConnections: info.TotalConnections,
Description: info.Description,
IconID: fmt.Sprintf("%d", info.IconID),
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "{}"
}
return string(data)
}
// ─── 数据库客户端列表 ──────────────────────────────────────
// ListDBClientsJSON 获取服务器数据库中的客户端列表(JSON 格式)
// start: 起始位置, duration: 返回数量(0=全部)
func (c *TSClient) ListDBClientsJSON(start, duration int) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
clients, err := client.ListDBClients(start, duration)
if err != nil {
log.Printf("[TSBridge] ListDBClients error: %v", err)
return "[]"
}
result := make([]dbClientJSON, len(clients))
for i, cl := range clients {
result[i] = dbClientJSON{
DBID: fmt.Sprintf("%d", cl.DBID),
UID: cl.UID,
Nickname: cl.Nickname,
Created: fmt.Sprintf("%d", cl.Created),
LastConnected: fmt.Sprintf("%d", cl.LastConnected),
TotalConnections: cl.TotalConnections,
Description: cl.Description,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// ─── Ban 管理 ─────────────────────────────────────────────
// ListBansJSON 获取 ban 列表(JSON 格式)
func (c *TSClient) ListBansJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
bans, err := client.ListBans()
if err != nil {
log.Printf("[TSBridge] ListBans error: %v", err)
return "[]"
}
result := make([]banEntryJSON, len(bans))
for i, b := range bans {
result[i] = banEntryJSON{
BanID: fmt.Sprintf("%d", b.BanID),
IP: b.IP,
Name: b.Name,
UID: b.UID,
Created: fmt.Sprintf("%d", b.Created),
InvokerName: b.InvokerName,
InvokerUID: b.InvokerUID,
Reason: b.Reason,
Enforcement: b.Enforcement,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// AddBan 添加 ban 条目。ip/name/uid 至少指定一个。timeSeconds=0 为永久。
// 返回空字符串表示成功。
func (c *TSClient) AddBan(ip, name, uid string, timeSeconds int, reason string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
if err := client.AddBan(ip, name, uid, timeSeconds, reason); err != nil {
return fmt.Sprintf("添加失败: %v", err)
}
return ""
}
// DeleteBan 删除 ban 条目。返回空字符串表示成功。
func (c *TSClient) DeleteBan(banIDStr string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var banID int64
if _, err := fmt.Sscanf(banIDStr, "%d", &banID); err != nil {
return fmt.Sprintf("无效的 ban ID: %v", err)
}
if err := client.DeleteBan(banID); err != nil {
return fmt.Sprintf("删除失败: %v", err)
}
return ""
}
// ─── Token 管理 ───────────────────────────────────────────
// ListTokensJSON 获取 token 列表(JSON 格式)
func (c *TSClient) ListTokensJSON() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
tokens, err := client.ListTokens()
if err != nil {
log.Printf("[TSBridge] ListTokens error: %v", err)
return "[]"
}
result := make([]tokenEntryJSON, len(tokens))
for i, t := range tokens {
result[i] = tokenEntryJSON{
Token: t.Token,
TokenType: t.TokenType,
TokenID1: fmt.Sprintf("%d", t.TokenID1),
TokenID2: fmt.Sprintf("%d", t.TokenID2),
Created: fmt.Sprintf("%d", t.Created),
Description: t.Description,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// UseToken 使用一个 privilege key。返回空字符串表示成功。
func (c *TSClient) UseToken(token string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
if err := client.UseToken(token); err != nil {
return fmt.Sprintf("使用失败: %v", err)
}
return ""
}
// ─── 文件列表 ─────────────────────────────────────────────
// ListFilesJSON 获取频道目录下的文件列表(JSON 格式)
// path: 虚拟路径,根目录为 "/"
func (c *TSClient) ListFilesJSON(channelIDStr, path string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return "[]"
}
files, err := client.ListFiles(channelID, path)
if err != nil {
log.Printf("[TSBridge] ListFiles error: %v", err)
return "[]"
}
result := make([]fileEntryJSON, len(files))
for i, f := range files {
result[i] = fileEntryJSON{
Name: f.Name,
Size: fmt.Sprintf("%d", f.Size),
DateTime: fmt.Sprintf("%d", f.DateTime),
IsFile: f.IsFile,
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// ─── 投诉管理 ─────────────────────────────────────────────
// ListComplaintsJSON 获取投诉列表(JSON 格式)
// targetDBIDStr: 目标客户端 DBID"0" 表示返回全部投诉
func (c *TSClient) ListComplaintsJSON(targetDBIDStr string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "[]"
}
var targetDBID uint64
if targetDBIDStr != "" && targetDBIDStr != "0" {
if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil {
return "[]"
}
}
complaints, err := client.ListComplaints(targetDBID)
if err != nil {
log.Printf("[TSBridge] ListComplaints error: %v", err)
return "[]"
}
result := make([]complaintEntryJSON, len(complaints))
for i, c := range complaints {
result[i] = complaintEntryJSON{
FromDBID: fmt.Sprintf("%d", c.FromDBID),
ToDBID: fmt.Sprintf("%d", c.ToDBID),
Message: c.Message,
Timestamp: fmt.Sprintf("%d", c.Timestamp),
}
}
data, err := json.Marshal(result)
if err != nil {
log.Printf("[TSBridge] JSON marshal error: %v", err)
return "[]"
}
return string(data)
}
// AddComplaint 提交投诉。返回空字符串表示成功。
func (c *TSClient) AddComplaint(targetDBIDStr, message string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var targetDBID uint64
if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil {
return fmt.Sprintf("无效的 DBID: %v", err)
}
if err := client.AddComplaint(targetDBID, message); err != nil {
return fmt.Sprintf("提交失败: %v", err)
}
return ""
}
// ─── 频道管理(创建/编辑/删除) ───────────────────────────
// CreateChannelJSON 创建新频道。propertiesJSON 为 JSON 对象,
// 包含可选的频道属性,如 channel_topic、channel_flag_permanent 等。
// 返回新频道的 ID 字符串,空字符串表示失败。
func (c *TSClient) CreateChannelJSON(name, propertiesJSON string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return ""
}
var props map[string]string
if propertiesJSON != "" && propertiesJSON != "{}" {
if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
log.Printf("[TSBridge] CreateChannel properties parse error: %v", err)
return ""
}
}
cid, err := client.CreateChannel(name, props)
if err != nil {
log.Printf("[TSBridge] CreateChannel error: %v", err)
return ""
}
return fmt.Sprintf("%d", cid)
}
// EditChannelJSON 编辑频道属性。propertiesJSON 为 JSON 对象。
// 返回空字符串表示成功,非空为错误。
func (c *TSClient) EditChannelJSON(channelIDStr, propertiesJSON string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return fmt.Sprintf("无效的频道 ID: %v", err)
}
var props map[string]string
if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
return fmt.Sprintf("属性解析失败: %v", err)
}
if err := client.EditChannel(channelID, props); err != nil {
return fmt.Sprintf("编辑失败: %v", err)
}
return ""
}
// DeleteChannel 删除频道。force=true 强制删除(含子频道)。
// 返回空字符串表示成功。
func (c *TSClient) DeleteChannel(channelIDStr string, force bool) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return fmt.Sprintf("无效的频道 ID: %v", err)
}
if err := client.DeleteChannel(channelID, force); err != nil {
return fmt.Sprintf("删除失败: %v", err)
}
return ""
}
// UpdateSelfJSON 更新当前客户端属性。propertiesJSON 为 JSON 对象,
// 如 {"client_away": "1", "client_away_message": "AFK"}。
// 返回空字符串表示成功。
func (c *TSClient) UpdateSelfJSON(propertiesJSON string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var props map[string]string
if err := json.Unmarshal([]byte(propertiesJSON), &props); err != nil {
return fmt.Sprintf("属性解析失败: %v", err)
}
if err := client.UpdateSelf(props); err != nil {
return fmt.Sprintf("更新失败: %v", err)
}
return ""
}
// KickClient 踢出客户端。reasonID: 4=频道踢出, 5=服务器踢出。
// 返回空字符串表示成功。
func (c *TSClient) KickClient(clid int, reasonID int, reasonMsg string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
if err := client.KickClient(uint16(clid), reasonID, reasonMsg); err != nil {
return fmt.Sprintf("踢出失败: %v", err)
}
return ""
}
// GetClientID 获取当前客户端 ID
func (c *TSClient) GetClientID() int {
c.mu.Lock()
defer c.mu.Unlock()
if c.client == nil {
return 0
}
return int(c.client.ClientID())
}
// GetChannelID 获取当前频道 ID
func (c *TSClient) GetChannelID() string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "0"
}
// 通过 GetClientInfo 获取当前频道 ID
info, err := client.GetClientInfo(client.ClientID())
if err != nil {
return "0"
}
if cid, ok := info["cid"]; ok {
return cid
}
return "0"
}
// SendChannelMessage 发送频道文字消息
// 返回空字符串表示成功
func (c *TSClient) SendChannelMessage(channelIDStr, message string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return fmt.Sprintf("无效的频道 ID: %v", err)
}
if err := client.SendTextMessage(2, channelID, message); err != nil {
return fmt.Sprintf("发送失败: %v", err)
}
return ""
}
// MoveToChannel 移动到指定频道
// 返回空字符串表示成功
func (c *TSClient) MoveToChannel(channelIDStr, password string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var channelID uint64
if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil {
return fmt.Sprintf("无效的频道 ID: %v", err)
}
if err := client.ClientMove(client.ClientID(), channelID, password); err != nil {
return fmt.Sprintf("移动失败: %v", err)
}
return ""
}
// SendTextMessage 发送文字消息
// targetMode: 1=私聊, 2=频道, 3=服务器
// 返回空字符串表示成功
func (c *TSClient) SendTextMessage(targetMode int, targetIDStr string, message string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var targetID uint64
if _, err := fmt.Sscanf(targetIDStr, "%d", &targetID); err != nil {
return fmt.Sprintf("无效的目标 ID: %v", err)
}
if err := client.SendTextMessage(targetMode, targetID, message); err != nil {
return fmt.Sprintf("发送失败: %v", err)
}
return ""
}
// Poke 向指定客户端发送 Poke 消息
// 返回空字符串表示成功
func (c *TSClient) Poke(clidStr string, message string) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
var clid uint64
if _, err := fmt.Sscanf(clidStr, "%d", &clid); err != nil {
return fmt.Sprintf("无效的客户端 ID: %v", err)
}
if err := client.Poke(uint16(clid), message); err != nil {
return fmt.Sprintf("Poke 失败: %v", err)
}
return ""
}
// setReceiveDecoderFactory installs the decoder provider used by
// StartReceiveAudio. It remains internal because VoiceDecoderFactory is a Go
// implementation detail and must not be exported through gomobile.
func (c *TSClient) ensureReceiveFactory() {
c.mu.Lock()
if c.receiveFactory == nil {
c.receiveFactory = newPlatformVoiceDecoderFactory()
log.Printf("[TSVoice] receive factory initialized lazily type=%T available=%t", c.receiveFactory, c.receiveFactory != nil)
}
c.mu.Unlock()
}
func (c *TSClient) setReceiveDecoderFactory(factory voiceDecoderFactory) {
c.mu.Lock()
c.receiveFactory = factory
if c.receive == nil && factory != nil {
c.receive = newReceiveAudio(factory, c.queueMixedPCM, c.queueSpeaking)
}
c.mu.Unlock()
}
// StartReceiveAudio starts the Go-owned receive pipeline when this platform
// provides a decoder factory. A fresh worker is created after a prior stop, so
// speaker toggles and reconnects can safely restart receiving.
func (c *TSClient) StartReceiveAudio() {
c.ensureReceiveFactory()
c.mu.Lock()
if c.receive == nil && c.receiveFactory != nil {
c.receive = newReceiveAudio(c.receiveFactory, c.queueMixedPCM, c.queueSpeaking)
}
r := c.receive
factoryAvailable := c.receiveFactory != nil
c.mu.Unlock()
log.Printf("[TSVoice] StartReceiveAudio worker=%t factory=%t", r != nil, factoryAvailable)
if r != nil {
r.start()
}
}
// StopReceiveAudio stops the receive worker and waits for it to release all
// decoders. Repeated calls are harmless.
func (c *TSClient) StopReceiveAudio() {
c.mu.Lock()
r := c.receive
c.receive = nil
c.mu.Unlock()
if r != nil {
r.stopAndWait()
}
}
// SetRemoteClientMuted excludes a remote client from the final mix while it
// continues receiving, decoding, and reporting speaking state.
func (c *TSClient) SetRemoteClientMuted(clientID int, muted bool) {
c.receiveCommand(audioCommand{kind: "mute", clientID: clientID, muted: muted})
}
func (c *TSClient) RemoveRemoteAudioClient(clientID int) {
c.receiveCommand(audioCommand{kind: "remove", clientID: clientID})
}
func (c *TSClient) ClearRemoteAudioClients() { c.receiveCommand(audioCommand{kind: "clear"}) }
func (c *TSClient) receiveCommand(cmd audioCommand) {
c.mu.Lock()
r := c.receive
c.mu.Unlock()
if r != nil {
r.command(cmd)
}
}
// SendVoice 发送 Opus 语音帧
// codec: 4=Opus Voice, 5=Opus Music
// 返回空字符串表示成功
func (c *TSClient) SendVoice(data []byte, codec int64) string {
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client == nil {
return "未连接"
}
if err := client.SendVoice(data, byte(codec)); err != nil {
return fmt.Sprintf("发送失败: %v", err)
}
return ""
}
// initEventQueue initializes a fresh consumer for this connection session.
func (c *TSClient) initEventQueue() {
c.mu.Lock()
callback := c.callback
c.mu.Unlock()
q := newEventQueue(callback)
c.evtQueueMu.Lock()
c.evtQueue = q
c.evtQueueMu.Unlock()
c.startEventLoop(q)
}
// stopEventQueue stops and joins the current consumer. It is safe to call
// repeatedly and prevents stale sessions from dispatching into a new callback.
func (c *TSClient) stopEventQueue() {
c.evtQueueMu.Lock()
q := c.evtQueue
c.evtQueue = nil
c.evtQueueMu.Unlock()
if q == nil {
return
}
q.stopOnce.Do(func() {
q.mu.Lock()
q.stopped = true
q.cond.Broadcast()
q.mu.Unlock()
})
}
// startEventLoop starts the callback consumer for q.
func (c *TSClient) startEventLoop(q *eventQueue) {
go func() {
for {
q.mu.Lock()
for len(q.events) == 0 && !q.stopped {
q.cond.Wait()
}
if q.stopped {
q.mu.Unlock()
return
}
events := q.events
q.events = nil
q.pcmCount = 0
q.mu.Unlock()
for _, evt := range events {
c.processEvent(q.callback, evt)
}
}
}()
}
// queueEvent queues an event only while its session's consumer remains active.
func (c *TSClient) queueEvent(evt queuedEvent) {
c.evtQueueMu.Lock()
q := c.evtQueue
c.evtQueueMu.Unlock()
if q == nil {
return
}
q.mu.Lock()
if !q.stopped {
q.events = append(q.events, evt)
q.cond.Signal()
}
q.mu.Unlock()
}
// processEvent 处理单个事件(在消费协程中顺序执行)
func (c *TSClient) processEvent(cb EventCallback, evt queuedEvent) {
if cb == nil {
return
}
switch evt.evtType {
case "connected":
log.Printf("[TSEvent] connected")
cb.OnConnected()
case "disconnected":
msg := evt.data.(string)
log.Printf("[TSEvent] disconnected: %q", msg)
cb.OnDisconnected(msg)
case "textmessage":
m := evt.data.(*TextMsg)
log.Printf("[TSEvent] textmessage from=%q uid=%q mode=%d target=%s msg=%q", m.InvokerName, m.InvokerUID, m.TargetMode, m.TargetID, m.Message)
cb.OnTextMessage(m)
case "cliententer":
cl := evt.data.(*Client)
log.Printf("[TSEvent] cliententer id=%d nick=%q uid=%q channel=%s", cl.ID, cl.Nickname, cl.UID, cl.ChannelID)
cb.OnClientEnter(cl)
case "clientleave":
d := evt.data.(struct {
ID int
ReasonMsg string
})
log.Printf("[TSEvent] clientleave id=%d reason=%q", d.ID, d.ReasonMsg)
cb.OnClientLeave(d.ID, d.ReasonMsg)
case "clientmoved":
d := evt.data.(struct {
ID int
TargetChannelID string
})
log.Printf("[TSEvent] clientmoved id=%d targetChannel=%s", d.ID, d.TargetChannelID)
cb.OnClientMoved(d.ID, d.TargetChannelID)
case "kicked":
reason := evt.data.(string)
log.Printf("[TSEvent] kicked reason=%q", reason)
cb.OnKicked(reason)
case "voicedata":
d := evt.data.(struct {
ClientID int
Data []byte
Codec int
Sequence int
IsWhisper bool
})
cb.OnVoiceData(d.ClientID, d.Data, d.Codec, d.Sequence, d.IsWhisper)
case "mixedvoicepcm":
cb.OnMixedVoicePCM(evt.data.([]byte))
case "clientspeaking":
d := evt.data.(struct {
ClientID int
Speaking bool
})
cb.OnClientSpeaking(d.ClientID, d.Speaking)
case "poked":
p := evt.data.(*PokeEvent)
log.Printf("[TSEvent] poked from=%q (id=%d uid=%q) msg=%q", p.InvokerName, p.InvokerID, p.InvokerUID, p.Message)
cb.OnPoked(p)
}
}
func (c *TSClient) queueMixedPCM(pcm []byte) {
c.evtQueueMu.Lock()
q := c.evtQueue
c.evtQueueMu.Unlock()
if q == nil {
return
}
q.mu.Lock()
if !q.stopped {
if q.pcmCount >= maxQueuedPCMEvents {
for i, evt := range q.events {
if evt.evtType == "mixedvoicepcm" {
copy(q.events[i:], q.events[i+1:])
q.events = q.events[:len(q.events)-1]
q.pcmCount--
break
}
}
}
q.events = append(q.events, queuedEvent{evtType: "mixedvoicepcm", data: pcm})
q.pcmCount++
q.cond.Signal()
}
q.mu.Unlock()
}
func (c *TSClient) queueSpeaking(clientID int, speaking bool) {
c.queueEvent(queuedEvent{evtType: "clientspeaking", data: struct {
ClientID int
Speaking bool
}{clientID, speaking}})
}
// registerEvents 注册事件处理
// 所有事件通过队列传递,在消费协程中顺序调用 JNI 回调
func (c *TSClient) isCurrentClient(client *ts.Client) bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.client == client
}
func (c *TSClient) queueClientEvent(client *ts.Client, evt queuedEvent) {
if c.isCurrentClient(client) {
c.queueEvent(evt)
}
}
func (c *TSClient) registerEvents(client *ts.Client) {
client.OnConnected(func() {
c.mu.Lock()
if c.client != client {
c.mu.Unlock()
return
}
c.connected = true
c.mu.Unlock()
c.queueEvent(queuedEvent{evtType: "connected"})
})
client.OnDisconnected(func(err error) {
c.StopReceiveAudio()
c.mu.Lock()
c.connected = false
c.client = nil
c.mu.Unlock()
msg := ""
if err != nil {
msg = err.Error()
}
c.queueEvent(queuedEvent{evtType: "disconnected", data: msg})
})
client.OnTextMessage(func(msg ts.TextMessage) {
c.queueEvent(queuedEvent{
evtType: "textmessage",
data: &TextMsg{
InvokerName: msg.InvokerName,
InvokerUID: msg.InvokerUID,
Message: msg.Message,
TargetMode: msg.TargetMode,
TargetID: fmt.Sprintf("%d", msg.TargetID),
},
})
})
client.OnClientEnter(func(info ts.ClientInfo) {
c.queueEvent(queuedEvent{
evtType: "cliententer",
data: &Client{
ID: int(info.ID),
Nickname: info.Nickname,
UID: info.UID,
ChannelID: fmt.Sprintf("%d", info.ChannelID),
IsSelf: false,
},
})
})
client.OnClientLeave(func(data ts.ClientLeftViewEvent) {
c.RemoveRemoteAudioClient(int(data.ID))
c.queueEvent(queuedEvent{
evtType: "clientleave",
data: struct {
ID int
ReasonMsg string
}{
ID: int(data.ID),
ReasonMsg: data.ReasonMsg,
},
})
})
client.OnClientMoved(func(data ts.ClientMovedEvent) {
c.queueEvent(queuedEvent{
evtType: "clientmoved",
data: struct {
ID int
TargetChannelID string
}{
ID: int(data.ID),
TargetChannelID: fmt.Sprintf("%d", data.TargetChannelID),
},
})
})
client.OnKicked(func(reason string) {
c.StopReceiveAudio()
c.mu.Lock()
c.connected = false
c.client = nil
c.mu.Unlock()
c.queueEvent(queuedEvent{evtType: "kicked", data: reason})
})
client.OnVoiceData(func(evt ts.VoiceDataEvent) {
c.mu.Lock()
r := c.receive
c.mu.Unlock()
if r != nil {
r.enqueue(int(evt.ClientID), int(evt.Codec), evt.Sequence, evt.Data)
return
}
// Keep the existing raw bridge active until an Android libopus decoder
// factory has been supplied and the Go receiver explicitly started.
c.queueEvent(queuedEvent{
evtType: "voicedata",
data: struct {
ClientID int
Data []byte
Codec int
Sequence int
IsWhisper bool
}{
ClientID: int(evt.ClientID),
Data: evt.Data,
Codec: int(evt.Codec),
Sequence: int(evt.Sequence),
IsWhisper: evt.IsWhisper,
},
})
})
client.OnPoked(func(evt ts.PokeEvent) {
c.queueEvent(queuedEvent{
evtType: "poked",
data: &PokeEvent{
InvokerID: int(evt.InvokerID),
InvokerName: evt.InvokerName,
InvokerUID: evt.InvokerUID,
Message: evt.Message,
},
})
})
}