// kotlin_api.go — Kotlin 友好 API 封装层 // // 本文件补充 bridge.go 中未暴露的 SDK 能力,并提供 Identity 管理接口。 // 与 bridge.go 保持相同的 gomobile 导出约定: // - 返回 string:空字符串=成功,非空=错误信息 // - 返回 JSON string:查询结果以 JSON 编码 // // 设计文档:docs/implementation/99_Go层Kotlin友好化重构.md // 能力清单:docs/implementation/98_Go层能力封装清单.md package teamspeak import ( "bytes" "context" "encoding/base64" "encoding/json" "fmt" "log" "sort" "strings" "time" ts "github.com/honeybbq/teamspeak-go" "github.com/honeybbq/teamspeak-go/crypto" ) // ═══════════════════════════════════════════════════════════════ // 一、Identity 管理 // ═══════════════════════════════════════════════════════════════ // GenerateIdentity 生成一个新的加密身份,返回序列化的字符串。 // Kotlin 侧应将此字符串持久化(SharedPreferences / DataStore), // 后续连接时通过 ConnectWithIdentity 复用,避免每次生成新身份。 // // securityLevel: 推荐值 8(2048-bit RSA)。值越大生成越慢。 // // 返回格式: "base64EncodedPrivateKey:offset" // 返回空字符串表示生成失败。 func GenerateIdentity(securityLevel int) string { identity, err := crypto.GenerateIdentity(securityLevel) if err != nil { log.Printf("[TSBridge] GenerateIdentity error: %v", err) return "" } return identity.String() } // ConnectWithIdentity 使用已有的 identity 连接到 TeamSpeak 服务器。 // identityStr 由 GenerateIdentity 生成并由 Kotlin 侧持久化。 // 其余参数和行为与 Connect 相同。 // // 返回:空字符串表示连接成功,非空为错误信息。 func (c *TSClient) ConnectWithIdentity(identityStr, host, nickname, password, defaultChannel, defaultChannelPassword string, cb EventCallback) string { c.Disconnect() c.stopEventQueue() c.mu.Lock() c.callback = cb c.connected = false c.host = host c.mu.Unlock() // 初始化事件队列和 Android decoder,避免旧的对象初始化路径留下 nil factory。 c.initEventQueue() c.ensureReceiveFactory() c.StartReceiveAudio() identity, err := crypto.IdentityFromString(identityStr) if err != nil { return fmt.Sprintf("identity 解析失败: %v", err) } // 计算 TeamSpeak UID(base64(SHA1(publicKey))),用于僵尸会话过滤。 // 不能直接存储 identity 字符串,因为 UID 是公钥的哈希,格式完全不同。 uid := crypto.GetUidFromPublicKey(identity.PublicKeyBase64()) c.mu.Lock() c.selfUID = uid c.mu.Unlock() 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) } // 延迟执行僵尸清理:连接刚建立时客户端在默认频道,ListClients 可能尚未 // 收到服务器发来的完整客户端列表。延迟 3 秒确保所有 notifycliententerview // 事件已到达,此时 ListClients 能发现不同频道中的僵尸会话。 go func() { time.Sleep(3 * time.Second) if c.isCurrentClient(client) { c.cleanupDuplicateIdentitySessions(client, identity) } }() return "" } // cleanupDuplicateIdentitySessions removes stale sessions using the same persisted // identity, while always preserving the newly connected session. func (c *TSClient) cleanupDuplicateIdentitySessions(client *ts.Client, identity *crypto.Identity) { clients, err := client.ListClients() if err != nil { log.Printf("[TSBridge] duplicate session scan failed: %v", err) return } uid := crypto.GetUidFromPublicKey(identity.PublicKeyBase64()) currentID := client.ClientID() for _, candidate := range clients { if candidate.ID == currentID || candidate.UID != uid { continue } if err := client.KickClient(candidate.ID, 5, "Replaced stale mobile session"); err != nil { log.Printf("[TSBridge] stale session kick failed clid=%d: %v", candidate.ID, err) } else { log.Printf("[TSBridge] stale session kicked clid=%d uid=%s", candidate.ID, uid) } } } // ═══════════════════════════════════════════════════════════════ // 二、查询能力(补充 bridge.go 未暴露的方法) // ═══════════════════════════════════════════════════════════════ // FindChannelsJSON 按名称模式搜索频道。 // 返回 JSON 数组:[{"id":"1","name":"匹配的频道"}, ...] // 空数组 "[]" 表示无匹配或出错。 func (c *TSClient) FindChannelsJSON(pattern string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } channels, err := client.FindChannels(pattern) if err != nil { log.Printf("[TSBridge] FindChannels error: %v", err) return "[]" } result := make([]channelJSON, len(channels)) for i, ch := range channels { result[i] = channelJSON{ ID: fmt.Sprintf("%d", ch.ID), Name: ch.Name, } } data, err := json.Marshal(result) if err != nil { log.Printf("[TSBridge] FindChannels JSON marshal error: %v", err) return "[]" } return string(data) } // FindClientByNameJSON 按昵称搜索数据库客户端。 // 返回 JSON 对象:{"uid":"xxx","dbid":"123"} // 空对象 "{}" 表示无匹配或出错。 func (c *TSClient) FindClientByNameJSON(nickname string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "{}" } uid, dbid, err := client.FindClientByName(nickname) if err != nil { log.Printf("[TSBridge] FindClientByName error: %v", err) return "{}" } result := map[string]string{ "uid": uid, "dbid": fmt.Sprintf("%d", dbid), } data, err := json.Marshal(result) if err != nil { return "{}" } return string(data) } // FindClientByDBIDJSON 按数据库 ID 查找客户端 UID。 // 返回 JSON 对象:{"uid":"xxx"} // 空对象 "{}" 表示无匹配或出错。 func (c *TSClient) FindClientByDBIDJSON(dbidStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "{}" } var dbid uint64 if _, err := fmt.Sscanf(dbidStr, "%d", &dbid); err != nil { return "{}" } uid, err := client.FindClientByDBID(dbid) if err != nil { log.Printf("[TSBridge] FindClientByDBID error: %v", err) return "{}" } result := map[string]string{"uid": uid} data, err := json.Marshal(result) if err != nil { return "{}" } return string(data) } // ListChannelsSortedJSON 获取去重+树形排序的详细频道列表。 // 使用 channellist -topic -flags -voice -limits -icon 获取完整数据, // 按 channel_order 排序,先顶层频道再子频道(树形扁平化)。 // 自动按 ID 去重(保留最后出现的条目)。 // 返回:JSON 数组,字段与 GetChannelsDetailedJSON 一致。 func (c *TSClient) ListChannelsSortedJSON() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "[]" } channels, err := client.ListChannelsDetailed() if err != nil { log.Printf("[KotlinAPI] ListChannelsSorted ListChannelsDetailed error: %v", err) return "[]" } // 去重:按 ID,保留最后出现的 seen := make(map[uint64]bool) deduped := make([]ts.ChannelInfoDetailed, 0, len(channels)) for _, ch := range channels { if seen[ch.ID] { continue } seen[ch.ID] = true deduped = append(deduped, ch) } // 按 ParentID 分组 children := make(map[uint64][]ts.ChannelInfoDetailed) var tops []ts.ChannelInfoDetailed for _, ch := range deduped { if ch.ParentID == 0 { tops = append(tops, ch) } else { children[ch.ParentID] = append(children[ch.ParentID], ch) } } // 按 Order 排序 sort.Slice(tops, func(i, j int) bool { return tops[i].Order < tops[j].Order }) for _, list := range children { sort.Slice(list, func(i, j int) bool { return list[i].Order < list[j].Order }) } // 树形扁平化:顶层 → 每个频道的子频道紧跟其后 var sorted []ts.ChannelInfoDetailed var flatten func(ch ts.ChannelInfoDetailed) flatten = func(ch ts.ChannelInfoDetailed) { sorted = append(sorted, ch) for _, child := range children[ch.ID] { flatten(child) } } for _, ch := range tops { flatten(ch) } // 转换为 JSON result := make([]channelDetailedJSON, len(sorted)) for i, ch := range sorted { result[i] = channelDetailedJSON{ ID: fmt.Sprintf("%d", ch.ID), ParentID: fmt.Sprintf("%d", ch.ParentID), Name: ch.Name, 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.IsOrdered, IconID: fmt.Sprintf("%d", ch.IconID), NeededModifyPower: ch.NeededModifyPower, } } data, err := json.Marshal(result) if err != nil { return "[]" } return string(data) } // ═══════════════════════════════════════════════════════════════ // 三、操作能力(补充 bridge.go 未暴露的方法) // ═══════════════════════════════════════════════════════════════ // MoveClient 将指定客户端移动到目标频道。 // clientID: 客户端 ID(clid) // channelID: 目标频道 ID // password: 频道密码(可选,无密码传空字符串) // 返回:空字符串表示成功,非空为错误信息。 func (c *TSClient) MoveClient(clientID int, 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(uint16(clientID), channelID, password); err != nil { return fmt.Sprintf("移动失败: %v", err) } return "" } // MoveChannel 移动频道到新的父频道或调整排序。 // channelID: 要移动的频道 ID // parentID: 新的父频道 ID(0 = 顶层) // order: 排序位置(0 = 最顶部) // 返回:空字符串表示成功,非空为错误信息。 func (c *TSClient) MoveChannel(channelIDStr, parentIDStr, orderStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var channelID, parentID, order uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return fmt.Sprintf("无效的频道 ID: %v", err) } if _, err := fmt.Sscanf(parentIDStr, "%d", &parentID); err != nil { return fmt.Sprintf("无效的父频道 ID: %v", err) } if _, err := fmt.Sscanf(orderStr, "%d", &order); err != nil { return fmt.Sprintf("无效的排序值: %v", err) } if err := client.MoveChannel(channelID, parentID, order); err != nil { return fmt.Sprintf("移动频道失败: %v", err) } return "" } // DeleteAllBans 清除所有封禁记录。 // 返回:空字符串表示成功,非空为错误信息。 func (c *TSClient) DeleteAllBans() string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } if err := client.DeleteAllBans(); err != nil { return fmt.Sprintf("清除失败: %v", err) } return "" } // DeleteComplaint 删除指定投诉。 // targetDBID: 被投诉者的数据库 ID // fromDBID: 投诉者的数据库 ID // 返回:空字符串表示成功,非空为错误信息。 func (c *TSClient) DeleteComplaint(targetDBIDStr, fromDBIDStr string) string { c.mu.Lock() client := c.client c.mu.Unlock() if client == nil { return "未连接" } var targetDBID, fromDBID uint64 if _, err := fmt.Sscanf(targetDBIDStr, "%d", &targetDBID); err != nil { return fmt.Sprintf("无效的目标 DBID: %v", err) } if _, err := fmt.Sscanf(fromDBIDStr, "%d", &fromDBID); err != nil { return fmt.Sprintf("无效的来源 DBID: %v", err) } if err := client.DeleteComplaint(targetDBID, fromDBID); err != nil { return fmt.Sprintf("删除投诉失败: %v", err) } return "" } // ═══════════════════════════════════════════════════════════════ // 四、文件传输(初始化阶段) // ═══════════════════════════════════════════════════════════════ // fileTransferInitJSON 文件传输初始化结果的 JSON 结构 type fileTransferInitJSON struct { Host string `json:"host"` Port uint16 `json:"port"` Key string `json:"key"` Size uint64 `json:"size,omitempty"` ClientFileTransferID uint16 `json:"clientFileTransferID"` ServerFileTransferID uint16 `json:"serverFileTransferID"` SeekPosition uint64 `json:"seekPosition,omitempty"` } // FileTransferInitUploadJSON 初始化文件上传。 // channelID: 目标频道 ID // path: 频道内虚拟路径(如 "/myfile.txt") // size: 文件大小(字节) // overwrite: 是否覆盖已有文件 // // 返回 JSON:{"port":0,"key":"...","clientFileTransferID":0,"serverFileTransferID":0,"seekPosition":0} // 空对象 "{}" 表示失败。 // // 注意:TCP 连接的 host 为当前服务器地址,port 从返回的 JSON 中获取。 func (c *TSClient) FileTransferInitUploadJSON(channelIDStr, path string, size int64, overwrite 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 "{}" } info, err := client.FileTransferInitUpload(channelID, path, "", uint64(size), overwrite) if err != nil { log.Printf("[TSBridge] FileTransferInitUpload error: %v", err) return "{}" } result := fileTransferInitJSON{ Port: info.Port, Key: info.FileTransferKey, ClientFileTransferID: info.ClientFileTransferID, ServerFileTransferID: info.ServerFileTransferID, SeekPosition: info.SeekPosition, } data, err := json.Marshal(result) if err != nil { return "{}" } return string(data) } // FileTransferInitDownloadJSON 初始化文件下载。 // channelID: 目标频道 ID // path: 频道内虚拟路径(如 "/myfile.txt") // // 返回 JSON:{"port":0,"key":"...","size":0,"clientFileTransferID":0,"serverFileTransferID":0} // 空对象 "{}" 表示失败。 func (c *TSClient) FileTransferInitDownloadJSON(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 "{}" } info, err := client.FileTransferInitDownload(channelID, path, "") if err != nil { log.Printf("[TSBridge] FileTransferInitDownload error: %v", err) return "{}" } result := fileTransferInitJSON{ Port: info.Port, Key: info.FileTransferKey, Size: info.Size, ClientFileTransferID: info.ClientFileTransferID, ServerFileTransferID: info.ServerFileTransferID, } data, err := json.Marshal(result) if err != nil { return "{}" } return string(data) } // DeleteFile 删除频道中的文件。 // channelID: 频道 ID // pathsJSON: 要删除的文件路径 JSON 数组,如 ["/file1.txt","/file2.txt"] // 返回:空字符串表示成功,非空为错误信息。 func (c *TSClient) DeleteFile(channelIDStr, pathsJSON 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 paths []string if err := json.Unmarshal([]byte(pathsJSON), &paths); err != nil { return fmt.Sprintf("路径解析失败: %v", err) } if err := client.FileTransferDeleteFile(channelID, paths); err != nil { return fmt.Sprintf("删除失败: %v", err) } return "" } // ═══════════════════════════════════════════════════════════════ // 4.5 文件下载(完整流程) // ═══════════════════════════════════════════════════════════════ // DownloadFileBytesJSON 下载频道文件并返回 base64 编码的内容。 // // 完整流程:ftinitdownload → TCP 连接 → 读取字节 → base64 编码。 // channelID: 频道 ID // path: 文件虚拟路径(如 "/image.png") // // 返回 JSON:{"data":"base64...","size":12345} // 空对象 "{}" 表示失败。 func (c *TSClient) DownloadFileBytesJSON(channelIDStr, path string) string { c.mu.Lock() client := c.client host := c.host c.mu.Unlock() if client == nil || host == "" { return "{}" } var channelID uint64 if _, err := fmt.Sscanf(channelIDStr, "%d", &channelID); err != nil { return "{}" } // 尝试多种路径格式(MyTS 文件可能存储在特殊目录) paths := []string{path} if !strings.HasPrefix(path, "/") { paths = append(paths, "/"+path) } for _, p := range paths { // 1. 初始化下载 info, err := client.FileTransferInitDownload(channelID, p, "") if err != nil { log.Printf("[TSBridge] DownloadFileBytes init error (path=%s): %v", p, err) continue } // 2. TCP 连接 + 下载 var buf bytes.Buffer if err := ts.DownloadFileData(host, info, &buf); err != nil { log.Printf("[TSBridge] DownloadFileBytes transfer error (path=%s): %v", p, err) continue } // 3. base64 编码(gomobile 不支持 []byte 返回) encoded := base64.StdEncoding.EncodeToString(buf.Bytes()) result := map[string]interface{}{ "data": encoded, "size": buf.Len(), } data, err := json.Marshal(result) if err != nil { return "{}" } return string(data) } return "{}" } // ═══════════════════════════════════════════════════════════════ // 五、批量查询(首次同步优化) // ═══════════════════════════════════════════════════════════════ // initialSyncJSON 首次同步结果的 JSON 结构 type initialSyncJSON struct { Channels []channelDetailedJSON `json:"channels"` Clients []clientJSON `json:"clients"` SelfID int `json:"selfId"` SelfChannelID string `json:"selfChannelId"` Server *serverInfoJSON `json:"server"` } // GetInitialSyncJSON 执行首次同步,一次性返回所有初始数据。 // 减少 JNI 调用次数(从 4 次降为 1 次)。 // // 返回 JSON: // // { // "channels": [...], // 详细频道列表 // "clients": [...], // 在线客户端列表 // "selfId": 1, // 自身客户端 ID // "selfChannelId": "1", // 自身所在频道 ID // "server": {...} // 服务器信息 // } // // 任一子查询失败时对应字段为 null/空,不影响其他字段。 func (c *TSClient) GetInitialSyncJSON() string { sync := initialSyncJSON{ SelfID: c.GetClientID(), SelfChannelID: c.GetChannelID(), } // 频道列表(详细) if data := c.GetChannelsDetailedJSON(); data != "[]" { var channels []channelDetailedJSON if err := json.Unmarshal([]byte(data), &channels); err == nil { sync.Channels = channels } } // 客户端列表 if data := c.GetClientsJSON(); data != "[]" { var clients []clientJSON if err := json.Unmarshal([]byte(data), &clients); err == nil { sync.Clients = clients } } // 服务器信息 if data := c.GetServerInfoJSON(); data != "{}" { var server serverInfoJSON if err := json.Unmarshal([]byte(data), &server); err == nil { sync.Server = &server } } data, err := json.Marshal(sync) if err != nil { log.Printf("[TSBridge] GetInitialSyncJSON marshal error: %v", err) return "{}" } return string(data) }