Files
go-ts/pkg/ts3client/client.go

392 lines
9.2 KiB
Go
Raw Normal View History

package ts3client
import (
"fmt"
"log"
"sync"
"time"
"go-ts/internal/client"
)
// Client is the main TeamSpeak 3 client
type Client struct {
address string
config Config
// Internal client
internal *client.Client
// Event handlers
handlers map[EventType][]any
mu sync.RWMutex
// State
connected bool
channels map[uint64]*Channel
clients map[uint16]*ClientInfo
serverInfo *ServerInfo
selfInfo *SelfInfo
channelsMu sync.RWMutex
clientsMu sync.RWMutex
}
// New creates a new TeamSpeak client
func New(address string, config Config) *Client {
// Apply defaults
if config.Nickname == "" {
config.Nickname = "GoTS3Bot"
}
if config.SecurityLevel == 0 {
config.SecurityLevel = 8
}
if config.Version == "" {
config.Version = "3.6.2 [Build: 1690976575]"
}
if config.Platform == "" {
config.Platform = "Windows"
}
if config.HWID == "" {
config.HWID = "1234567890"
}
return &Client{
address: address,
config: config,
handlers: make(map[EventType][]any),
channels: make(map[uint64]*Channel),
clients: make(map[uint16]*ClientInfo),
}
}
// On registers an event handler
// The handler function signature must match the event type:
// - EventConnected: func(*ConnectedEvent)
// - EventMessage: func(*MessageEvent)
// - EventAudio: func(*AudioEvent)
// - etc.
func (c *Client) On(event EventType, handler any) {
c.mu.Lock()
defer c.mu.Unlock()
c.handlers[event] = append(c.handlers[event], handler)
}
// emit calls all handlers registered for the given event
func (c *Client) emit(event EventType, data any) {
c.mu.RLock()
handlers := c.handlers[event]
c.mu.RUnlock()
for _, h := range handlers {
switch event {
case EventConnected:
if fn, ok := h.(func(*ConnectedEvent)); ok {
fn(data.(*ConnectedEvent))
}
case EventDisconnected:
if fn, ok := h.(func(*DisconnectedEvent)); ok {
fn(data.(*DisconnectedEvent))
}
case EventMessage:
if fn, ok := h.(func(*MessageEvent)); ok {
fn(data.(*MessageEvent))
}
case EventClientEnter:
if fn, ok := h.(func(*ClientEnterEvent)); ok {
fn(data.(*ClientEnterEvent))
}
case EventClientLeft:
if fn, ok := h.(func(*ClientLeftEvent)); ok {
fn(data.(*ClientLeftEvent))
}
case EventClientMoved:
if fn, ok := h.(func(*ClientMovedEvent)); ok {
fn(data.(*ClientMovedEvent))
}
case EventChannelList:
if fn, ok := h.(func(*ChannelListEvent)); ok {
fn(data.(*ChannelListEvent))
}
case EventAudio:
if fn, ok := h.(func(*AudioEvent)); ok {
fn(data.(*AudioEvent))
}
case EventError:
if fn, ok := h.(func(*ErrorEvent)); ok {
fn(data.(*ErrorEvent))
}
}
}
}
// Connect establishes a connection to the TeamSpeak server
// This method blocks until disconnected or an error occurs
func (c *Client) Connect() error {
c.internal = client.NewClient(c.config.Nickname)
// Set event callback on internal client
c.internal.SetEventHandler(c.handleInternalEvent)
log.Printf("Connecting to %s as %s...", c.address, c.config.Nickname)
err := c.internal.Connect(c.address)
if err != nil {
c.emit(EventDisconnected, &DisconnectedEvent{Reason: err.Error()})
log.Printf("[TS3Client] Connect returning with error: %v", err)
return err
}
log.Printf("[TS3Client] Connect returning cleanly")
return nil
}
// ConnectAsync connects in the background and returns immediately
func (c *Client) ConnectAsync() <-chan error {
errChan := make(chan error, 1)
go func() {
if err := c.Connect(); err != nil {
errChan <- err
}
close(errChan)
}()
// Give it a moment to start
time.Sleep(100 * time.Millisecond)
return errChan
}
// Disconnect closes the connection gracefully
func (c *Client) Disconnect() {
log.Println("[Disconnect] Starting disconnect sequence...")
if c.internal != nil {
// Send disconnect command to server
log.Println("[Disconnect] Sending disconnect command...")
c.sendDisconnect("leaving")
// Wait for packet to be sent and ACKed - the internal loop must still be running
log.Println("[Disconnect] Waiting for disconnect to be processed...")
time.Sleep(1000 * time.Millisecond)
// Stop the internal loop
log.Println("[Disconnect] Stopping internal loop...")
c.internal.Stop()
if c.internal.Conn != nil {
log.Println("[Disconnect] Closing connection...")
c.internal.Conn.Close()
}
}
c.connected = false
log.Println("[Disconnect] Done")
c.emit(EventDisconnected, &DisconnectedEvent{Reason: "client disconnect"})
}
// sendDisconnect sends the disconnect command to the server
func (c *Client) sendDisconnect(reason string) {
if c.internal == nil {
return
}
// Use internal client's SendCommand
cmd := "clientdisconnect reasonid=8 reasonmsg=" + escapeTS3(reason)
log.Printf("Sending disconnect: %s", cmd)
if err := c.internal.SendCommandString(cmd); err != nil {
log.Printf("Error sending disconnect: %v", err)
}
}
type disconnectCommand struct {
reason string
}
func (d *disconnectCommand) encode() string {
return "clientdisconnect reasonid=8 reasonmsg=" + escapeTS3(d.reason)
}
func escapeTS3(s string) string {
// Basic escape for TS3 protocol
result := ""
for _, r := range s {
switch r {
case '\\':
result += "\\\\"
case '/':
result += "\\/"
case ' ':
result += "\\s"
case '|':
result += "\\p"
default:
result += string(r)
}
}
return result
}
// IsConnected returns true if the client is connected
func (c *Client) IsConnected() bool {
return c.connected
}
// handleInternalEvent processes events from the internal client
func (c *Client) handleInternalEvent(eventType string, data map[string]any) {
switch eventType {
case "connected":
c.connected = true
clientID := uint16(0)
serverName := ""
if v, ok := data["clientID"].(uint16); ok {
clientID = v
}
if v, ok := data["serverName"].(string); ok {
serverName = v
}
c.selfInfo = &SelfInfo{ClientID: clientID, Nickname: c.config.Nickname}
c.serverInfo = &ServerInfo{Name: serverName}
c.emit(EventConnected, &ConnectedEvent{
ClientID: clientID,
ServerName: serverName,
})
case "message":
targetMode := MessageTarget(1)
if v, ok := data["targetMode"].(int); ok {
targetMode = MessageTarget(v)
}
c.emit(EventMessage, &MessageEvent{
SenderID: getUint16(data, "senderID"),
SenderName: getString(data, "senderName"),
Message: getString(data, "message"),
TargetMode: targetMode,
})
case "client_enter":
info := &ClientInfo{
ID: getUint16(data, "clientID"),
Nickname: getString(data, "nickname"),
ChannelID: getUint64(data, "channelID"),
}
c.clientsMu.Lock()
c.clients[info.ID] = info
c.clientsMu.Unlock()
c.emit(EventClientEnter, &ClientEnterEvent{
ClientID: info.ID,
Nickname: info.Nickname,
ChannelID: info.ChannelID,
})
case "client_left":
clientID := getUint16(data, "clientID")
c.clientsMu.Lock()
delete(c.clients, clientID)
c.clientsMu.Unlock()
c.emit(EventClientLeft, &ClientLeftEvent{
ClientID: clientID,
Reason: getString(data, "reason"),
})
case "client_moved":
clientID := getUint16(data, "clientID")
channelID := getUint64(data, "channelID")
c.clientsMu.Lock()
if client, ok := c.clients[clientID]; ok {
client.ChannelID = channelID
}
c.clientsMu.Unlock()
// Update selfInfo if it's us
if c.selfInfo != nil && c.selfInfo.ClientID == clientID {
c.selfInfo.ChannelID = channelID
}
c.emit(EventClientMoved, &ClientMovedEvent{
ClientID: clientID,
ChannelID: channelID,
})
case "channel_list":
if channels, ok := data["channels"].([]*client.Channel); ok {
c.channelsMu.Lock()
var chList []*Channel
for _, ch := range channels {
converted := &Channel{
ID: ch.ID,
ParentID: ch.ParentID,
Name: ch.Name,
Order: ch.Order,
}
c.channels[ch.ID] = converted
chList = append(chList, converted)
}
c.channelsMu.Unlock()
c.emit(EventChannelList, &ChannelListEvent{Channels: chList})
}
case "audio":
c.emit(EventAudio, &AudioEvent{
SenderID: getUint16(data, "senderID"),
Codec: AudioCodec(getInt(data, "codec")),
PCM: getPCM(data, "pcm"),
Channels: getInt(data, "channels"),
})
case "error":
c.emit(EventError, &ErrorEvent{
ID: getString(data, "id"),
Message: getString(data, "message"),
})
}
}
// Helper functions for type conversion
func getString(m map[string]any, key string) string {
if v, ok := m[key].(string); ok {
return v
}
return ""
}
func getUint16(m map[string]any, key string) uint16 {
if v, ok := m[key].(uint16); ok {
return v
}
if v, ok := m[key].(int); ok {
return uint16(v)
}
return 0
}
func getUint64(m map[string]any, key string) uint64 {
if v, ok := m[key].(uint64); ok {
return v
}
if v, ok := m[key].(int); ok {
return uint64(v)
}
return 0
}
func getInt(m map[string]any, key string) int {
if v, ok := m[key].(int); ok {
return v
}
return 0
}
func getPCM(m map[string]any, key string) []int16 {
if v, ok := m[key].([]int16); ok {
return v
}
return nil
}
// WaitForConnection waits until the client is connected or timeout
func (c *Client) WaitForConnection(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if c.connected {
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("connection timeout")
}