Files
ts-mobile-go/go/teamspeak/kotlin_api.go
T
2026-07-23 19:37:20 +08:00

702 lines
22 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.
// 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)
}
// 计算 TeamSpeak UIDbase64(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: 客户端 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)
}