feat: Implement core voicebot functionality with TeamSpeak 3 and xAI integration.

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-16 10:39:27 +01:00
parent aa8c0dbcbc
commit fb17813dcb
10 changed files with 460 additions and 287 deletions

View File

@@ -23,6 +23,31 @@ func ParseCommand(data []byte) (string, map[string]string) {
return cmd, args
}
// ParseCommands parses response that may contain multiple items separated by pipe (|)
func ParseCommands(data []byte) []*Command {
s := string(data)
// TS3 uses pipe | to separate list items
items := strings.Split(s, "|")
cmds := make([]*Command, 0, len(items))
// First item contains the command name
name, args := ParseCommand([]byte(items[0]))
cmds = append(cmds, &Command{Name: name, Params: args})
// Subsequent items reuse the same command name
for _, item := range items[1:] {
// Hack: Prepend command name to reuse ParseCommand logic
// or better: manually parse args.
// Since ParseCommand splits by space, we can just use "DUMMY " + item
// ensuring we trim properly.
_, itemArgs := ParseCommand([]byte("CMD " + strings.TrimSpace(item)))
cmds = append(cmds, &Command{Name: name, Params: itemArgs})
}
return cmds
}
// Unescape TS3 string
func Unescape(s string) string {
r := strings.NewReplacer(

View File

@@ -132,9 +132,11 @@ func (c *Client) Connect() error {
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
}
@@ -154,18 +156,24 @@ func (c *Client) ConnectAsync() <-chan error {
// 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")
// Small delay to allow packet to be sent
time.Sleep(100 * time.Millisecond)
// 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"})
}

View File

@@ -129,6 +129,35 @@ func (c *Client) SendAudio(pcm []int16) error {
return c.sendJSON(msg)
}
// SendText sends a text message to trigger a Grok response
func (c *Client) SendText(text string) error {
// Create conversation item with text
createMsg := ConversationItemCreate{
Type: "conversation.item.create",
Item: ConversationItem{
Type: "message",
Role: "user",
Content: []ItemContent{
{Type: "input_text", Text: text},
},
},
}
if err := c.sendJSON(createMsg); err != nil {
return err
}
// Request response
responseMsg := ResponseCreate{
Type: "response.create",
Response: ResponseSettings{
Modalities: []string{"text", "audio"},
},
}
return c.sendJSON(responseMsg)
}
// Close closes the WebSocket connection
func (c *Client) Close() {
c.mu.Lock()
@@ -179,6 +208,13 @@ func (c *Client) receiveLoop() {
_, message, err := c.conn.ReadMessage()
if err != nil {
// Check if closed intentionally
select {
case <-c.done:
return
default:
}
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
log.Println("[xAI] Connection closed normally")
} else {