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

687 lines
21 KiB
Go
Raw Normal View History

2026-07-20 19:01:03 +08:00
// 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: 推荐值 82048-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)
}
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)
}
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: 客户端 IDclid
// 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: 新的父频道 ID0 = 顶层)
// 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)
}