From 3a57f41fc20f273c8f20f64ea2317ec522bbd7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Luis=20Monta=C3=B1es=20Ojados?= Date: Sat, 17 Jan 2026 00:53:50 +0100 Subject: [PATCH] Implement Poke functionality, refine talking status and add local log events --- cmd/tui/model.go | 49 ++++++++++++++++++++++++++++++++++++- internal/client/client.go | 49 ++++++++++++++++++++++++------------- internal/client/commands.go | 26 ++++++++++++++++++-- pkg/ts3client/client.go | 11 +++++++++ pkg/ts3client/events.go | 10 ++++++++ 5 files changed, 125 insertions(+), 20 deletions(-) diff --git a/cmd/tui/model.go b/cmd/tui/model.go index c705231..f96f995 100644 --- a/cmd/tui/model.go +++ b/cmd/tui/model.go @@ -183,6 +183,16 @@ type clientLeftMsg struct { clientID uint16 } +type clientMovedMsg struct { + clientID uint16 + channelID uint64 +} + +type pokeMsg struct { + senderName string + message string +} + type chatMsg struct { senderID uint16 senderName string @@ -240,6 +250,20 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.audioPlayer.PlayPCM(e.SenderID, e.PCM) } }) + + m.client.On(ts3client.EventClientMoved, func(e *ts3client.ClientMovedEvent) { + m.program.Send(clientMovedMsg{ + clientID: e.ClientID, + channelID: e.ChannelID, + }) + }) + + m.client.On(ts3client.EventPoke, func(e *ts3client.PokeEvent) { + m.program.Send(pokeMsg{ + senderName: e.SenderName, + message: e.Message, + }) + }) } // Initialize audio player @@ -349,6 +373,29 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, nil + case clientMovedMsg: + if msg.clientID == m.selfID { + chName := "Unknown" + if ch := m.client.GetChannel(msg.channelID); ch != nil { + chName = ch.Name + } + m.chatMessages = append(m.chatMessages, ChatMessage{ + Time: time.Now(), + Sender: "SYSTEM", + Content: fmt.Sprintf("You moved to channel: %s", chName), + }) + } + return m, nil + + case pokeMsg: + m.chatMessages = append(m.chatMessages, ChatMessage{ + Time: time.Now(), + Sender: "POKE", + Content: fmt.Sprintf("[%s]: %s", msg.senderName, msg.message), + }) + m.addLog("Received poke from %s: %s", msg.senderName, msg.message) + return m, nil + case chatMsg: m.chatMessages = append(m.chatMessages, ChatMessage{ Time: time.Now(), @@ -569,7 +616,7 @@ func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } -func (m *Model) handleChatKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { +func (m *Model) handleChatKeys(_ tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } diff --git a/internal/client/client.go b/internal/client/client.go index 6e8890f..3670514 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -134,26 +134,41 @@ func (c *Client) Connect(address string) error { defer ticker.Stop() for { + // Recovery from panics in the main loop + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("PANIC in Client loop: %v", r) + } + }() + + select { + case <-c.done: + log.Println("Client loop stopped") + return + case pkt := <-pktChan: + if pkt == nil { + // Channel closed + return + } + if err := c.handlePacket(pkt); err != nil { + log.Printf("Error handling packet: %v", err) + } + case <-ticker.C: + if !c.Connected { + return // Don't send pings if not connected yet + } + // Send KeepAlive Ping (Encrypted, No NewProtocol) + if err := c.sendPing(); err != nil { + log.Printf("Error sending Ping: %v", err) + } + } + }() + // Check if we should exit after the inner function select { case <-c.done: - log.Println("Client loop stopped") return nil - case pkt := <-pktChan: - if pkt == nil { - // Channel closed - return nil - } - if err := c.handlePacket(pkt); err != nil { - log.Printf("Error handling packet: %v", err) - } - case <-ticker.C: - if !c.Connected { - continue // Don't send pings if not connected yet - } - // Send KeepAlive Ping (Encrypted, No NewProtocol) - if err := c.sendPing(); err != nil { - log.Printf("Error sending Ping: %v", err) - } + default: } } } diff --git a/internal/client/commands.go b/internal/client/commands.go index 9104d55..c39c45b 100644 --- a/internal/client/commands.go +++ b/internal/client/commands.go @@ -226,8 +226,8 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error { cmdStr := string(data) // Debug: Log packet flags and raw command preview (sanitized) - log.Printf("Debug Packet: Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q", - pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data), + log.Printf("Debug Packet: PID=%d, Compressed=%v, Fragmented=%v, RawLen=%d, Preview=%q", + pkt.Header.PacketID, pkt.Header.FlagCompressed(), pkt.Header.FlagFragmented(), len(data), func() string { preview := cmdStr if len(preview) > 100 { @@ -544,6 +544,28 @@ func (c *Client) processCommand(data []byte, pkt *protocol.Packet) error { "message": msg, }) + case "notifyclientpoke": + msg := "" + invoker := "Unknown" + var invokerID uint16 + if m, ok := args["msg"]; ok { + msg = protocol.Unescape(m) + } + if name, ok := args["invokername"]; ok { + invoker = protocol.Unescape(name) + } + if iid, ok := args["invokerid"]; ok { + var id uint64 + fmt.Sscanf(iid, "%d", &id) + invokerID = uint16(id) + } + log.Printf("[Poke] %s: %s", invoker, msg) + c.emitEvent("client_poke", map[string]any{ + "senderID": invokerID, + "senderName": invoker, + "message": msg, + }) + case "notifyservergrouplist", "notifychannelgrouplist", "notifyclientneededpermissions": // Ignore verbose noisy setup commands default: diff --git a/pkg/ts3client/client.go b/pkg/ts3client/client.go index c95f218..cb562dc 100644 --- a/pkg/ts3client/client.go +++ b/pkg/ts3client/client.go @@ -124,6 +124,10 @@ func (c *Client) emit(event EventType, data any) { if fn, ok := h.(func(*TalkingStatusEvent)); ok { fn(data.(*TalkingStatusEvent)) } + case EventPoke: + if fn, ok := h.(func(*PokeEvent)); ok { + fn(data.(*PokeEvent)) + } } } } @@ -405,6 +409,13 @@ func (c *Client) handleInternalEvent(eventType string, data map[string]any) { Channels: getInt(data, "channels"), }) + case "client_poke": + c.emit(EventPoke, &PokeEvent{ + SenderID: getUint16(data, "senderID"), + SenderName: getString(data, "senderName"), + Message: getString(data, "message"), + }) + case "error": c.emit(EventError, &ErrorEvent{ ID: getString(data, "id"), diff --git a/pkg/ts3client/events.go b/pkg/ts3client/events.go index 9d61c91..40d0965 100644 --- a/pkg/ts3client/events.go +++ b/pkg/ts3client/events.go @@ -25,8 +25,18 @@ const ( // Error events EventError EventType = "error" + + // Poke events + EventPoke EventType = "poke" ) +// PokeEvent is emitted when a poke message is received +type PokeEvent struct { + SenderID uint16 + SenderName string + Message string +} + // ConnectedEvent is emitted when the client successfully connects type ConnectedEvent struct { ClientID uint16