diff --git a/cmd/tui/model.go b/cmd/tui/model.go index 19b6f36..0801013 100644 --- a/cmd/tui/model.go +++ b/cmd/tui/model.go @@ -104,6 +104,11 @@ type Model struct { isMuted bool // Mic muted isPTT bool // Push-to-talk active + // Popup State + showPokePopup bool + pokePopupSender string + pokePopupMessage string + // Program reference for sending messages from event handlers program *tea.Program showLog bool @@ -398,12 +403,19 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case pokeMsg: + // Append to chat as well 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) + + // Trigger Popup + m.showPokePopup = true + m.pokePopupSender = msg.senderName + m.pokePopupMessage = msg.message + return m, nil case chatMsg: @@ -545,8 +557,18 @@ func (m *Model) updateChannelList(channels []*ts3client.Channel) { } func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + key := msg.String() + + // Global Key Handling for Popup + if m.showPokePopup { + if key == "esc" || key == "enter" || key == "q" { + m.showPokePopup = false + } + return m, nil + } + // 1. Absolute Globals (Always active) - switch msg.String() { + switch key { case "ctrl+c": if m.client != nil { m.client.Disconnect() @@ -836,6 +858,32 @@ func (m *Model) handleInputKeys(msg tea.KeyMsg) (tea.Model, tea.Cmd) { // View renders the UI func (m *Model) View() string { + if m.showPokePopup { + boxStyle := lipgloss.NewStyle(). + Border(lipgloss.DoubleBorder()). + BorderForeground(lipgloss.Color("196")). // Red for Poke + Padding(1, 2). + Width(50). + Align(lipgloss.Center) + + titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("226")).MarginBottom(1) + senderStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("208")).Bold(true) + msgStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Italic(true) + helpStyle := lipgloss.NewStyle().Faint(true).MarginTop(2) + + content := lipgloss.JoinVertical(lipgloss.Center, + titleStyle.Render("YOU WERE POKED!"), + "", + fmt.Sprintf("From: %s", senderStyle.Render(m.pokePopupSender)), + "", + msgStyle.Render(fmt.Sprintf("%q", m.pokePopupMessage)), + "", + helpStyle.Render("(Press Esc/Enter to close)"), + ) + + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, boxStyle.Render(content)) + } + if m.focus == FocusAbout { return m.renderAboutView() } diff --git a/internal/client/client.go b/internal/client/client.go index 3670514..c6c2b13 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -36,6 +36,7 @@ type Client struct { PingPacketID uint16 // Type 0x04 PongPacketID uint16 // Type 0x05 AckPacketID uint16 // Type 0x06 + AckLowPacketID uint16 // Type 0x07 // Ping RTT tracking PingSentTimes map[uint16]time.Time // Map PingPacketID -> Time sent @@ -48,10 +49,14 @@ type Client struct { ServerName string // Fragment reassembly (packet queue like ts3j) - CommandQueue map[uint16]*protocol.Packet // Packets waiting for reassembly - ExpectedCommandPID uint16 // Next expected packet ID + CommandQueue map[uint16]*protocol.Packet // Packets waiting for reassembly (Type 0x02) + ExpectedCommandPID uint16 // Next expected packet ID (Type 0x02) FragmentState bool // Toggle: true = collecting, false = ready + CommandLowQueue map[uint16]*protocol.Packet // Packets waiting for reassembly (Type 0x03) + ExpectedCommandLowPID uint16 // Next expected packet ID (Type 0x03) + FragmentStateLow bool // Toggle: true = collecting, false = ready + // Server Data Channels map[uint64]*Channel @@ -69,17 +74,20 @@ type Client struct { func NewClient(nickname string) *Client { return &Client{ - Nickname: nickname, - PacketIDCounterC2S: 1, - VoicePacketID: 1, - Channels: make(map[uint64]*Channel), - VoiceDecoders: make(map[uint16]*opus.Decoder), - CommandQueue: make(map[uint16]*protocol.Packet), - ExpectedCommandPID: 0, - PingSentTimes: make(map[uint16]time.Time), - PingRTT: 0, - PingDeviation: 0, - done: make(chan struct{}), + Nickname: nickname, + PacketIDCounterC2S: 1, + AckLowPacketID: 1, + VoicePacketID: 1, + Channels: make(map[uint64]*Channel), + VoiceDecoders: make(map[uint16]*opus.Decoder), + CommandQueue: make(map[uint16]*protocol.Packet), + ExpectedCommandPID: 0, + CommandLowQueue: make(map[uint16]*protocol.Packet), + ExpectedCommandLowPID: 0, + PingSentTimes: make(map[uint16]time.Time), + PingRTT: 0, + PingDeviation: 0, + done: make(chan struct{}), } } diff --git a/internal/client/commands.go b/internal/client/commands.go index c39c45b..42b1227 100644 --- a/internal/client/commands.go +++ b/internal/client/commands.go @@ -35,6 +35,21 @@ func sanitizeForLog(s string) string { } func (c *Client) handleCommand(pkt *protocol.Packet) error { + // Select the correct queue and counters based on PacketType + var queue map[uint16]*protocol.Packet + var expectedPID *uint16 + var fragmentState *bool + + if pkt.Header.PacketType() == protocol.PacketTypeCommandLow { + queue = c.CommandLowQueue + expectedPID = &c.ExpectedCommandLowPID + fragmentState = &c.FragmentStateLow + } else { + queue = c.CommandQueue + expectedPID = &c.ExpectedCommandPID + fragmentState = &c.FragmentState + } + // Check if Encrypted // PacketTypeCommand is usually encrypted. // Flag check? The flag is in the Header (e.g. Unencrypted flag). @@ -100,14 +115,14 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { // Queue-based fragment reassembly (like ts3j) // Store packet in queue - c.CommandQueue[pkt.Header.PacketID] = &protocol.Packet{ + queue[pkt.Header.PacketID] = &protocol.Packet{ Header: pkt.Header, Data: append([]byte{}, data...), // Clone data (already decrypted) } // Try to process packets in order for { - nextPkt, ok := c.CommandQueue[c.ExpectedCommandPID] + nextPkt, ok := queue[*expectedPID] if !ok { // Missing packet, wait for it break @@ -117,16 +132,16 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { if isFragmented { // Toggle fragment state - c.FragmentState = !c.FragmentState + *fragmentState = !*fragmentState - if c.FragmentState { + if *fragmentState { // Starting a new fragment sequence // Don't process yet, wait for more - c.ExpectedCommandPID++ + *expectedPID++ continue } else { // Ending fragment sequence - reassemble all - reassembled, compressed := c.reassembleFragments() + reassembled, compressed := c.reassembleFragmentsCustom(queue, *expectedPID) if reassembled == nil { log.Printf("Fragment reassembly failed") break @@ -144,9 +159,9 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { } } } - } else if c.FragmentState { + } else if *fragmentState { // Middle fragment - keep collecting - c.ExpectedCommandPID++ + *expectedPID++ continue } else { // Non-fragmented packet - process normally @@ -165,10 +180,11 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { } // Remove processed packet from queue - delete(c.CommandQueue, c.ExpectedCommandPID) - c.ExpectedCommandPID++ + delete(queue, *expectedPID) + *expectedPID++ // Process the command + // Fix: processCommand should probably handle execution if err := c.processCommand(data, nextPkt); err != nil { log.Printf("Error processing command: %v", err) } @@ -177,16 +193,17 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error { return nil } -// reassembleFragments collects all buffered fragments in order and returns reassembled data -func (c *Client) reassembleFragments() ([]byte, bool) { +// reassembleFragmentsCustom collects all buffered fragments in order from the given queue +// ending at currentPID. +func (c *Client) reassembleFragmentsCustom(queue map[uint16]*protocol.Packet, currentPID uint16) ([]byte, bool) { var result []byte compressed := false // Find the start of the fragment sequence (scan backwards from current) - startPID := c.ExpectedCommandPID + startPID := currentPID for { prevPID := startPID - 1 - pkt, ok := c.CommandQueue[prevPID] + pkt, ok := queue[prevPID] if !ok { break } @@ -198,9 +215,9 @@ func (c *Client) reassembleFragments() ([]byte, bool) { startPID = prevPID } - // Now collect from startPID to ExpectedCommandPID (inclusive) - for pid := startPID; pid <= c.ExpectedCommandPID; pid++ { - pkt, ok := c.CommandQueue[pid] + // Now collect from startPID to currentPID (inclusive) + for pid := startPID; pid <= currentPID; pid++ { + pkt, ok := queue[pid] if !ok { log.Printf("Missing fragment PID=%d during reassembly", pid) return nil, false @@ -212,11 +229,11 @@ func (c *Client) reassembleFragments() ([]byte, bool) { } result = append(result, pkt.Data...) - delete(c.CommandQueue, pid) + delete(queue, pid) } log.Printf("Reassembled fragments PID %d-%d, total %d bytes, compressed=%v", - startPID, c.ExpectedCommandPID, len(result), compressed) + startPID, currentPID, len(result), compressed) return result, compressed } diff --git a/internal/client/packet.go b/internal/client/packet.go index 3db9113..2448323 100644 --- a/internal/client/packet.go +++ b/internal/client/packet.go @@ -55,6 +55,46 @@ func (c *Client) handlePacket(pkt *protocol.Packet) error { c.Conn.SendPacket(ack) + return c.handleCommand(pkt) + case protocol.PacketTypeCommandLow: + // Send ACK Low + ackData := make([]byte, 2) + binary.BigEndian.PutUint16(ackData, pkt.Header.PacketID) + + ack := protocol.NewPacket(protocol.PacketTypeAckLow, ackData) + // Spec/ts3j: AckLow has its own counter + c.AckLowPacketID++ + ack.Header.PacketID = c.AckLowPacketID + ack.Header.ClientID = c.ClientID + // ACKs usually don't have NewProtocol flag set in Header byte + ack.Header.Type &= ^uint8(protocol.PacketFlagNewProtocol) + + // ACKs for Command packets after handshake must be encrypted + key := protocol.HandshakeKey + nonce := protocol.HandshakeNonce + + if c.Handshake != nil && c.Handshake.Step >= 6 && len(c.Handshake.SharedIV) > 0 { + crypto := &protocol.CryptoState{ + SharedIV: c.Handshake.SharedIV, + SharedMac: c.Handshake.SharedMac, + GenerationID: 0, + } + key, nonce = crypto.GenerateKeyNonce(&ack.Header, true) // Client->Server=true + } + + // Meta for Client->Server: PID(2) + CID(2) + PT(1) = 5 bytes + meta := make([]byte, 5) + binary.BigEndian.PutUint16(meta[0:2], ack.Header.PacketID) + binary.BigEndian.PutUint16(meta[2:4], ack.Header.ClientID) + meta[4] = ack.Header.Type + + encData, mac, _ := protocol.EncryptEAX(key, nonce, meta, ack.Data) + ack.Data = encData + copy(ack.Header.MAC[:], mac) + // log.Printf("Sending ACK Low for server CommandLow PID=%d", pkt.Header.PacketID) + + c.Conn.SendPacket(ack) + return c.handleCommand(pkt) case protocol.PacketTypeVoice: c.handleVoice(pkt) diff --git a/scripts/run.ps1 b/scripts/run.ps1 index 5f3d101..ecb4f7d 100644 --- a/scripts/run.ps1 +++ b/scripts/run.ps1 @@ -4,4 +4,4 @@ $env:PKG_CONFIG_PATH = "D:\esto_al_path\msys64\mingw64\lib\pkgconfig" Write-Host "Starting TeamSpeak Client (Windows Native)..." -ForegroundColor Cyan # go run ./cmd/client/main.go --server localhost:9987 # go run ./cmd/example --server localhost:9987 -go run ./cmd/tui --server ts.vlazaro.es:9987 --nickname Adam --debug +go run ./cmd/tui --server 127.0.0.1:9987 --nickname Adam --debug