首次推送
This commit is contained in:
@@ -0,0 +1,686 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
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: 客户端 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)
|
||||
}
|
||||
Reference in New Issue
Block a user