Files
ts-mobile-go/go/teamspeak/bridge.go
T
2026-07-20 19:01:03 +08:00

1559 lines
42 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
},
})
})
}