1559 lines
42 KiB
Go
1559 lines
42 KiB
Go
// 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,
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|