首次推送
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
package teamspeak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/honeybbq/teamspeak-go/commands"
|
||||
"github.com/honeybbq/teamspeak-go/handshake"
|
||||
"github.com/honeybbq/teamspeak-go/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
errTeamSpeakCommand = errors.New("TeamSpeak server error")
|
||||
errCommandTimed = errors.New("command timeout")
|
||||
voicePacketCount uint64
|
||||
)
|
||||
|
||||
type commandResult struct {
|
||||
Err error
|
||||
Data []map[string]string
|
||||
}
|
||||
|
||||
// commandTracker matches return_code values to pending commands and response rows.
|
||||
type commandTracker struct {
|
||||
pending map[uint32]chan commandResult
|
||||
collecting map[uint32][]map[string]string
|
||||
mu sync.Mutex
|
||||
nextRC uint32
|
||||
}
|
||||
|
||||
func newCommandTracker() *commandTracker {
|
||||
return &commandTracker{
|
||||
pending: make(map[uint32]chan commandResult),
|
||||
collecting: make(map[uint32][]map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *commandTracker) register() (uint32, <-chan commandResult) {
|
||||
rc := atomic.AddUint32(&t.nextRC, 1)
|
||||
ch := make(chan commandResult, 1)
|
||||
t.mu.Lock()
|
||||
t.pending[rc] = ch
|
||||
t.mu.Unlock()
|
||||
|
||||
return rc, ch
|
||||
}
|
||||
|
||||
func (t *commandTracker) unregister(rc uint32) {
|
||||
t.mu.Lock()
|
||||
delete(t.pending, rc)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// collect appends a parameter row to the pending command with the largest return_code.
|
||||
func (t *commandTracker) collect(params map[string]string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
var maxRC uint32
|
||||
for rc := range t.pending {
|
||||
if rc > maxRC {
|
||||
maxRC = rc
|
||||
}
|
||||
}
|
||||
if maxRC > 0 {
|
||||
t.collecting[maxRC] = append(t.collecting[maxRC], params)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *commandTracker) resolve(rc uint32, err error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if ch, ok := t.pending[rc]; ok {
|
||||
data := t.collecting[rc]
|
||||
delete(t.collecting, rc)
|
||||
ch <- commandResult{Data: data, Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *commandTracker) reset() {
|
||||
t.mu.Lock()
|
||||
t.pending = make(map[uint32]chan commandResult)
|
||||
t.collecting = make(map[uint32][]map[string]string)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) handlePacket(p *transport.Packet) {
|
||||
c.logger.Debug("received packet", slog.Uint64("type", uint64(p.Type())), slog.Int("length", len(p.Data)))
|
||||
switch p.Type() {
|
||||
case transport.PacketTypeInit1:
|
||||
c.logger.Debug("processing init1 packet")
|
||||
response := handshake.ProcessInit1(c.crypt, p.Data)
|
||||
if response != nil {
|
||||
c.logger.Debug("sending init1 response")
|
||||
err := c.handler.SendPacket(byte(transport.PacketTypeInit1), response, 0)
|
||||
if err != nil {
|
||||
c.logger.Warn("failed to send init1 response", slog.Any("error", err))
|
||||
}
|
||||
}
|
||||
case transport.PacketTypeCommand, transport.PacketTypeCommandLow:
|
||||
if len(p.Data) == 0 {
|
||||
return
|
||||
}
|
||||
dataStr := string(p.Data)
|
||||
c.logger.Debug("received command data", slog.String("data", dataStr))
|
||||
c.handleCommandLines(dataStr)
|
||||
case transport.PacketTypeVoice, transport.PacketTypeVoiceWhisper:
|
||||
// Voice payload format: [packetID(2 BE)] [clientID(2 BE)] [codec(1)] [opusData...].
|
||||
if len(p.Data) >= 5 {
|
||||
packetID := uint16(p.Data[0])<<8 | uint16(p.Data[1])
|
||||
clientID := uint16(p.Data[2])<<8 | uint16(p.Data[3])
|
||||
codec := p.Data[4]
|
||||
count := atomic.AddUint64(&voicePacketCount, 1)
|
||||
if count <= 5 || count%250 == 0 {
|
||||
log.Printf("[TSVoice] packet count=%d client=%d codec=%d sequence=%d payload=%d whisper=%t", count, clientID, codec, packetID, len(p.Data)-5, p.Type() == transport.PacketTypeVoiceWhisper)
|
||||
}
|
||||
opusData := make([]byte, len(p.Data)-5)
|
||||
copy(opusData, p.Data[5:])
|
||||
c.notifyEvent(VoiceDataEvent{
|
||||
ClientID: clientID,
|
||||
Data: opusData,
|
||||
Codec: codec,
|
||||
Sequence: packetID,
|
||||
IsWhisper: p.Type() == transport.PacketTypeVoiceWhisper,
|
||||
})
|
||||
}
|
||||
case transport.PacketTypePing, transport.PacketTypePong,
|
||||
transport.PacketTypeAck, transport.PacketTypeAckLow:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleCommandLines(s string) {
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
start := 0
|
||||
for i := 0; i <= len(s); i++ {
|
||||
if i == len(s) || s[i] == '\n' || s[i] == 0x00 {
|
||||
part := strings.TrimSuffix(s[start:i], "\r")
|
||||
if part != "" {
|
||||
rows := splitCommandRows(part)
|
||||
for _, row := range rows {
|
||||
c.handleCommand(row)
|
||||
}
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleCommand(s string) {
|
||||
cmd := commands.ParseCommand(s)
|
||||
if cmd == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Nameless rows are the data rows of list responses (clientlist,
|
||||
// channellist), which arrive as bare key=value pairs. Collect them for the
|
||||
// pending command instead of dropping them.
|
||||
if cmd.Name == "" {
|
||||
if len(cmd.Params) > 0 {
|
||||
c.cmdTrack.collect(cmd.Params)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Debug("processing command", slog.String("name", cmd.Name), slog.Any("params", cmd.Params))
|
||||
|
||||
if strings.HasPrefix(cmd.Name, "notify") {
|
||||
c.handleNotification(cmd)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch cmd.Name {
|
||||
case "clientinitiv":
|
||||
c.handleHandshakeInitIV(cmd)
|
||||
case "initivexpand2":
|
||||
c.handleHandshakeExpand2(cmd)
|
||||
case "initserver":
|
||||
c.handleInitServer(cmd)
|
||||
case "error":
|
||||
c.handleError(cmd)
|
||||
default:
|
||||
c.cmdTrack.collect(cmd.Params)
|
||||
c.logger.Debug("unhandled or data command", slog.String("name", cmd.Name), slog.Any("params", cmd.Params))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleError(cmd *commands.Command) {
|
||||
id := cmd.Params["id"]
|
||||
msg := cmd.Params["msg"]
|
||||
rcStr := cmd.Params["return_code"]
|
||||
|
||||
var err error
|
||||
if id != "0" {
|
||||
err = fmt.Errorf("%w: %s (id=%s)", errTeamSpeakCommand, msg, id)
|
||||
c.logger.Error("server returned error", slog.String("id", id), slog.String("message", msg))
|
||||
|
||||
// Fatal server errors (e.g. banned) during the handshake are handled
|
||||
// below via signalConnected + transport close. The separate
|
||||
// go c.Disconnect() is only needed when already fully connected.
|
||||
if id == "3329" {
|
||||
c.mu.Lock()
|
||||
isConnecting := c.status == StatusConnecting
|
||||
c.mu.Unlock()
|
||||
if !isConnecting {
|
||||
c.logger.Warn("fatal connection error detected, closing connection", slog.String("id", id))
|
||||
go func() {
|
||||
disconnectErr := c.Disconnect()
|
||||
if disconnectErr != nil {
|
||||
c.logger.Warn("disconnect after fatal error failed", slog.Any("error", disconnectErr))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handshake-phase errors (e.g. password error 1028, banned 3329) arrive
|
||||
// without a return_code. When we are still in the connecting phase,
|
||||
// store the error, unblock WaitConnected, and close the transport so
|
||||
// the connection is cleaned up promptly.
|
||||
if rcStr == "" && id != "0" {
|
||||
c.mu.Lock()
|
||||
isConnecting := c.status == StatusConnecting
|
||||
c.mu.Unlock()
|
||||
if isConnecting {
|
||||
c.logger.Info("handshake error detected, unblocking WaitConnected",
|
||||
slog.String("id", id), slog.String("message", msg))
|
||||
c.signalConnected(err)
|
||||
// Close transport to trigger handleConnectionClosed and clean up.
|
||||
// handleConnectionClosed will fire disconnected handlers with the
|
||||
// stored handshake error.
|
||||
go func() {
|
||||
if closeErr := c.handler.Close(); closeErr != nil {
|
||||
c.logger.Warn("transport close after handshake error failed", slog.Any("error", closeErr))
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if rcStr != "" {
|
||||
rc, parseErr := strconv.ParseUint(rcStr, 10, 32)
|
||||
if parseErr == nil {
|
||||
c.cmdTrack.resolve(uint32(rc), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SendCommandNoWait sends a command without waiting for return_code.
|
||||
func (c *Client) SendCommandNoWait(cmd string) error {
|
||||
err := c.throttle.wait(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.logger.Debug("sending command without waiting", slog.String("raw", cmd))
|
||||
|
||||
return c.finalCmdHandler(cmd)
|
||||
}
|
||||
|
||||
// ExecCommand sends a command and waits for its return_code response.
|
||||
func (c *Client) ExecCommand(cmd string, timeout time.Duration) error {
|
||||
_, err := c.ExecCommandWithResponse(cmd, timeout)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecCommandWithResponse sends a command and waits for its return_code response and data.
|
||||
func (c *Client) ExecCommandWithResponse(cmd string, timeout time.Duration) ([]map[string]string, error) {
|
||||
rc, ch := c.cmdTrack.register()
|
||||
defer c.cmdTrack.unregister(rc)
|
||||
|
||||
withReturnCode := cmd
|
||||
if !strings.Contains(cmd, "return_code=") {
|
||||
withReturnCode = fmt.Sprintf("%s return_code=%d", cmd, rc)
|
||||
}
|
||||
|
||||
c.logger.Debug("sending command", slog.String("raw", withReturnCode))
|
||||
|
||||
err := c.throttle.wait(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.finalCmdHandler(withReturnCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case res := <-ch:
|
||||
return res.Data, res.Err
|
||||
case <-time.After(timeout):
|
||||
return nil, fmt.Errorf("%w: %s", errCommandTimed, cmd)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user