# teamspeak-go

纯 Go 实现的 TeamSpeak 3 客户端协议库，零 CGO 依赖，支持 gomobile 编译为 Android `.aar`。

- GitHub: https://github.com/honeybbq/teamspeak-go
- 本项目使用本地补丁版本（`go/_patches/`），通过 `go.mod` 的 `replace` 指令生效

## Architecture

```
┌──────────────────────────────────────────────┐
│  Application Layer                           │
│  (bridge.go / 用户代码)                       │
│  OnConnected, OnTextMessage, SendVoice ...   │
├──────────────────────────────────────────────┤
│  Client (client.go)                          │
│  连接管理、事件分发、命令追踪                    │
│  events.go — 事件注册 & dispatchEvent         │
│  commands.go — 命令发送 & return_code 匹配     │
│  notifications.go — notify* 事件解析           │
│  api.go — 高层 API (ListChannels, SendText...)│
│  handshake.go — 加密握手 & clientinit          │
│  transfer.go — 文件传输                        │
│  throttle.go — 命令限速 (token bucket)         │
├──────────────────────────────────────────────┤
│  Transport (transport/)                      │
│  PacketHandler — UDP 收发、包分片              │
│  packet.go — 包类型定义                        │
│  quicklz.go — QuickLZ 压缩解压                │
├──────────────────────────────────────────────┤
│  Crypto (crypto/)                            │
│  Identity 生成、密钥交换、EAX 加密              │
├──────────────────────────────────────────────┤
│  Handshake (handshake/)                      │
│  crypt_init2.go — 二次加密协商                 │
│  license.go — 许可证验证                       │
├──────────────────────────────────────────────┤
│  Discovery (discovery/)                      │
│  SRV / TSDNS / 直连 地址解析                   │
└──────────────────────────────────────────────┘
```

### Key Files

| 文件 | 职责 |
|------|------|
| `client.go` | `Client` 结构体、`NewClient`、`Connect`、`Disconnect`、事件循环 (`startEventLoop`)、`notifyEvent` 入队 |
| `events.go` | 事件注册 API (`OnConnected`, `OnTextMessage` 等)、`dispatchEvent` 分发到所有 handler |
| `notifications.go` | `handleNotification` — 解析 `notify*` 命令，转换为结构体事件 |
| `commands.go` | `handlePacket` — 包类型路由、`ExecCommand` / `ExecCommandWithResponse` — 带 `return_code` 的异步命令 |
| `api.go` | 高层 API：`ListChannels`、`ListClients`、`GetClientInfo`、`SendTextMessage`、`ClientMove`、`Poke`、`SendVoice`、`WaitConnected` |
| `handshake.go` | `handleHandshakeInitIV`、`handleHandshakeExpand2`、`handleInitServer`、`sendClientInit` |
| `types.go` | 所有事件/数据结构体定义 |
| `transfer.go` | 文件传输：`FileTransferInitUpload`、`FileTransferInitDownload`、`FileTransferDeleteFile` |
| `throttle.go` | Token-bucket 命令限速器（4 tokens/s，上限 8） |

### Event Loop

所有事件在单个 goroutine 中串行处理，保证按到达顺序分发：

```
notifyEvent(evt)          ← 任何 goroutine 调用
  → 入队 (evtQueueMu)
  → signal (evtCond)
      ↓
startEventLoop goroutine  ← 唯一消费者
  → 取出全部待处理事件
  → dispatchEvent(evt)    ← 逐个分发到注册的 handler
```

## Event Lifecycle

事件分为两类：**服务器通知事件**（`notify` 前缀，服务器主动推送）和**内部事件**（SDK 状态变化触发）。

### Phase 1: Connection (连接阶段)

客户端从发起到建立连接的握手过程，涉及加密协商和身份初始化。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| `PacketTypeInit1` | 握手包 | 初始化加密通道 | 客户端发送 UDP 包后服务器返回 init1 响应，完成第一阶段密钥交换。SDK 内部处理，应用层无感 |
| `clientinitiv` | 服务器响应 | 加密参数协商 | 服务器返回 `alpha`、`beta`、`omega` 加密参数，客户端调用 `InitCrypto` 初始化加密上下文 |
| `initivexpand2` | 服务器响应 | 二次加密扩展 | 服务器返回许可证和扩展密钥，客户端生成临时密钥对（`clientek`）完成最终加密握手 |
| `clientinit` | 客户端命令 | 发送身份信息 | 加密握手完成后，客户端发送昵称、版本、HWID、默认频道等信息，请求加入服务器 |
| `initserver` | 服务器响应 | 连接建立确认 | 服务器分配客户端 ID（`clid`），连接正式建立。触发 `OnConnected` 回调 |
| **`OnConnected`** | SDK 回调 | 连接成功通知 | 应用层注册的连接成功回调。SDK 自动发送 `clientupdate` 解除静音。此时可调用 `ListChannels`、`ListClients` 获取快照 |

### Phase 2: Runtime — User Events (用户事件)

服务器上用户的上下线、移动、消息等实时事件。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| **`notifycliententerview`** → `OnClientEnter` | 服务器通知 | 用户进入视野 | 有新客户端连接到服务器或进入可见范围。数据结构 `ClientInfo`：`ID`（clid）、`Nickname`、`UID`、`ChannelID`、`Type`、`ServerGroups` |
| **`notifyclientleftview`** → `OnClientLeave` | 服务器通知 | 用户离开视野 | 客户端断开连接或离开可见范围。数据结构 `ClientLeftViewEvent`：`ID`、`ReasonID`（0=正常、4=频道踢出、5=服务器踢出）、`ReasonMsg` |
| **`notifyclientmoved`** → `OnClientMoved` | 服务器通知 | 用户切换频道 | 客户端被移动到另一个频道。数据结构 `ClientMovedEvent`：`ID`、`TargetChannelID`、`ReasonID`、`InvokerID`、`InvokerName`、`InvokerUID`。**如果是自己被移动，这是频道切换的确认点** |
| **`notifytextmessage`** → `OnTextMessage` | 服务器通知 | 收到文字消息 | 收到私聊/频道/服务器范围的文字消息。数据结构 `TextMessage`：`TargetMode`（1=私聊、2=频道、3=服务器）、`TargetID`、`InvokerID`、`InvokerName`、`InvokerUID`、`Message`、`InvokerGroups` |
| **`notifyclientpoke`** → `OnPoked` | 服务器通知 | 被其他用户戳 | 有用户发送 poke 消息。数据结构 `PokeEvent`：`InvokerID`、`InvokerName`、`InvokerUID`、`Message` |
| `notifyclientneededpermissions` | 服务器通知 | 权限不足 | 操作因权限不足被拒绝。携带 `permid`、`permvalue`。SDK 仅记录日志，不触发应用层回调 |

### Phase 3: Runtime — Voice (语音事件)

实时语音数据的收发，通过 UDP 二进制包传输。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| **`VoiceDataEvent`** → `OnVoiceData` | 二进制包 | 收到语音数据 | 其他客户端发送的 Opus 语音帧。数据结构 `VoiceDataEvent`：`ClientID`（发送者）、`Data`（Opus 数据）、`Codec`（4=Opus Voice、5=Opus Music） |
| `SendVoice(data, codec)` | 客户端命令 | 发送语音数据 | 发送 Opus 编码的语音帧到服务器，服务器转发给同频道其他用户。通过 `handler.SendVoicePacket` 直接写入 UDP |

**语音包二进制格式：**

```
Offset  Size   Field
0       2      packetID (big-endian)
2       2      clientID (little-endian)
4       1      codec (4=Opus Voice, 5=Opus Music)
5       N      Opus encoded data
```

### Phase 4: Runtime — File Transfer (文件传输事件)

服务器文件的上传、下载和管理。文件数据通过独立的 TCP 连接传输。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| `ftinitupload` / **`notifystartupload`** | 命令+通知 | 初始化文件上传 | 客户端请求上传文件，服务器返回 `FileUploadInfo`：`Port`（TCP 端口）、`FileTransferKey`（传输密钥）、`SeekPosition`（断点）、`ClientFileTransferID`、`ServerFileTransferID` |
| `ftinitdownload` / **`notifystartdownload`** | 命令+通知 | 初始化文件下载 | 客户端请求下载文件，服务器返回 `FileDownloadInfo`：`Port`、`FileTransferKey`、`Size`、`ClientFileTransferID`、`ServerFileTransferID` |
| **`notifystatusfiletransfer`** | 服务器通知 | 文件传输状态 | 传输完成或失败的状态通知。数据结构 `FileTransferStatusInfo`：`Status`（0=成功）、`Message`（错误信息）、`ClientFileTransferID` |
| `ftdeletefile` | 客户端命令 | 删除服务器文件 | 删除指定频道目录下的文件，支持 `\|` 分隔的批量删除 |

### Phase 5: Runtime — Commands & Errors (命令与错误)

客户端命令的异步响应机制和错误处理。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| `return_code` | 服务器响应 | 命令执行结果 | 每个命令附带 `return_code` 参数，服务器执行完成后返回对应的 `return_code`，SDK 通过 `commandTracker` 匹配异步响应。`id=0` 表示成功，非 0 为错误码 |
| **`error`** | 服务器响应 | 服务器错误 | 服务器返回的错误信息。`id`（错误码）、`msg`（错误描述）。**错误码 3329 为致命错误（如被 ban），SDK 自动断开连接** |
| `clientupdate` | 客户端命令 | 更新客户端状态 | 连接成功后 SDK 自动发送，设置 `client_input_muted=0`、`client_output_muted=0` |

**命令限速：** SDK 内置 token-bucket 限速器（`throttle.go`），速率 4 tokens/s，上限 8。所有 `ExecCommand` 调用自动排队等待。

### Phase 6: Disconnect (断开阶段)

连接关闭和清理。

| 事件名 | 类型 | 作用 | 描述 |
|--------|------|------|------|
| `clientdisconnect` | 客户端命令 | 主动断开 | 客户端发送 `clientdisconnect reasonmsg=Shutdown`，然后关闭 UDP 连接 |
| **`OnDisconnected`** | SDK 回调 | 断开连接通知 | 连接断开时触发。参数 `error`：`nil` = 正常断开，非 `nil` = 异常断开原因 |
| `OnClosed` (handler) | 传输层回调 | 底层连接关闭 | UDP 连接关闭时触发，SDK 内部据此调用 `OnDisconnected`。应用层不直接使用 |

### Lifecycle Overview (生命周期总览)

```
NewClient(identity, addr, nickname, opts...)
  │
  ├─ 启动事件循环协程 (startEventLoop)
  ├─ 初始化 PacketHandler、Crypto、Throttle
  │
  Connect()
  │
  ├─ 地址解析 (SRV / TSDNS / 直连)
  ├─ UDP 连接
  ├─ PacketTypeInit1 握手
  ├─ ← clientinitiv (加密参数)
  ├─ → InitCrypto
  ├─ ← initivexpand2 (二次加密)
  ├─ → clientek (临时密钥)
  ├─ → clientinit (身份信息)
  ├─ ← initserver → 连接建立
  │
  ├─ → OnConnected() 回调
  ├─ → clientupdate (解除静音)
  │
  │   ┌─ API 调用 ─────────────────────────────┐
  │   │ ListChannels()  → channellist           │
  │   │ ListClients()   → clientlist            │
  │   │ GetClientInfo() → clientinfo            │
  │   │ SendTextMessage() → sendtextmessage     │
  │   │ ClientMove()    → clientmove            │
  │   │ SendVoice()     → UDP 二进制包           │
  │   └─────────────────────────────────────────┘
  │
  │   ┌─ 服务器事件 ────────────────────────────┐
  │   │ notifycliententerview → OnClientEnter   │
  │   │ notifyclientleftview  → OnClientLeave   │
  │   │ notifyclientmoved     → OnClientMoved   │
  │   │ notifytextmessage     → OnTextMessage   │
  │   │ notifyclientpoke      → OnPoked         │
  │   │ VoiceDataEvent        → OnVoiceData     │
  │   │ error (id=3329)       → 自动断开         │
  │   └─────────────────────────────────────────┘
  │
  Disconnect()
  │
  ├─ → clientdisconnect reasonmsg=Shutdown
  ├─ → 关闭 UDP 连接 (handler.Close)
  └─ → OnDisconnected(nil) 回调
```

## Data Types

| 结构体 | 用途 | 关键字段 |
|--------|------|----------|
| `ClientInfo` | 客户端信息 | `ID` (uint16), `Nickname`, `UID`, `ChannelID` (uint64), `Type`, `ServerGroups` ([]string) |
| `ChannelInfo` | 频道信息 | `ID` (uint64), `Name`, `ParentID` (uint64), `Description` |
| `TextMessage` | 文字消息 | `TargetMode` (int), `TargetID` (uint64), `InvokerID` (uint16), `InvokerName`, `InvokerUID`, `Message`, `InvokerGroups` |
| `ClientMovedEvent` | 频道移动 | `ID` (uint16), `TargetChannelID` (uint64), `ReasonID` (int), `InvokerID` (uint16), `InvokerName`, `InvokerUID` |
| `ClientLeftViewEvent` | 用户离开 | `ID` (uint16), `ReasonID` (int), `ReasonMsg`, `TargetID` (uint16) |
| `PokeEvent` | Poke 消息 | `InvokerID` (uint16), `InvokerName`, `InvokerUID`, `Message` |
| `VoiceDataEvent` | 语音数据 | `ClientID` (uint16), `Data` ([]byte), `Codec` (byte) |
| `FileUploadInfo` | 上传信息 | `Port`, `FileTransferKey`, `SeekPosition`, `ClientFileTransferID`, `ServerFileTransferID` |
| `FileDownloadInfo` | 下载信息 | `Port`, `FileTransferKey`, `Size`, `ClientFileTransferID`, `ServerFileTransferID` |
| `FileTransferStatusInfo` | 传输状态 | `Status` (int), `Message`, `ClientFileTransferID` |

## API Reference

### Client Construction

```go
identity, _ := crypto.GenerateIdentity(8)
client := teamspeak.NewClient(identity, "host:9987", "Nickname",
    teamspeak.WithServerPassword("pass"),
    teamspeak.WithDefaultChannel("/General"),
    teamspeak.WithDefaultChannelPassword("cpw"),
    teamspeak.WithLogger(logger),
    teamspeak.WithResolver(customResolver),
)
```

### Connection

```go
client.Connect()                          // 启动连接（非阻塞）
client.WaitConnected(context.Background()) // 阻塞等待握手完成
client.Disconnect()                       // 优雅断开
client.ClientID()                         // 获取服务器分配的客户端 ID
```

### Data Queries

```go
channels, err := client.ListChannels()    // 获取所有频道
clients, err := client.ListClients()      // 获取所有在线客户端
info, err := client.GetClientInfo(clid)   // 获取单个客户端详情
```

### Messaging

```go
client.SendTextMessage(targetMode, targetID, message) // 发送文字消息
client.ClientMove(clid, channelID, password)           // 移动频道
client.Poke(clid, message)                             // Poke 用户
```

### Voice

```go
client.SendVoice(opusData, 4) // 发送 Opus Voice 帧 (codec=4)
client.SendVoice(opusData, 5) // 发送 Opus Music 帧 (codec=5)
```

### File Transfer

```go
info, err := client.FileTransferInitUpload(channelID, "/path", "", size, false)
teamspeak.UploadFileData(host, info, reader)

info, err := client.FileTransferInitDownload(channelID, "/path", "")
teamspeak.DownloadFileData(host, info, writer)

client.FileTransferDeleteFile(channelID, []string{"/file1", "/file2"})
```

### Event Registration

```go
client.OnConnected(func() { ... })
client.OnDisconnected(func(err error) { ... })
client.OnClientEnter(func(info teamspeak.ClientInfo) { ... })
client.OnClientLeave(func(evt teamspeak.ClientLeftViewEvent) { ... })
client.OnClientMoved(func(evt teamspeak.ClientMovedEvent) { ... })
client.OnTextMessage(func(msg teamspeak.TextMessage) { ... })
client.OnPoked(func(evt teamspeak.PokeEvent) { ... })
client.OnKicked(func(reason string) { ... })
client.OnVoiceData(func(evt teamspeak.VoiceDataEvent) { ... })
```

### Middleware

```go
// 命令中间件：拦截/修改出站命令
client.UseCommandMiddleware(func(next func(string) error) func(string) error {
    return func(cmd string) error {
        log.Println("CMD:", cmd)
        return next(cmd)
    }
})

// 事件中间件：拦截/修改入站事件
client.UseEventMiddleware(func(next func(any)) func(any) {
    return func(evt any) {
        log.Println("EVT:", evt)
        next(evt)
    }
})
```

## Local Patches

本项目通过 `go.mod` 的 `replace` 指令使用本地补丁版本：

```
replace github.com/honeybbq/teamspeak-go => ./_patches/github.com/honeybbq/teamspeak-go
```

补丁修复了上游库的以下问题：
- 32 位目标平台的整数溢出（`math.MaxUint32 overflows int`）
- 其他 gomobile 兼容性修复
