702 lines
22 KiB
Go
702 lines
22 KiB
Go
// 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)
|
||
}
|