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") }