160 lines
3.5 KiB
Go
160 lines
3.5 KiB
Go
package client
|
|
|
|
import (
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"go-ts/pkg/protocol"
|
|
"go-ts/pkg/transport"
|
|
|
|
"gopkg.in/hraban/opus.v2"
|
|
)
|
|
|
|
type Channel struct {
|
|
ID uint64
|
|
ParentID uint64
|
|
Name string
|
|
Order uint64
|
|
}
|
|
|
|
// EventHandler is called when events occur
|
|
type EventHandler func(eventType string, data map[string]any)
|
|
|
|
type Client struct {
|
|
Conn *transport.TS3Conn
|
|
Handshake *HandshakeState
|
|
|
|
Nickname string
|
|
ClientID uint16
|
|
|
|
// Counters
|
|
PacketIDCounterC2S uint16
|
|
VoicePacketID uint16
|
|
|
|
// State
|
|
Connected bool
|
|
ServerName string
|
|
|
|
// Fragment reassembly
|
|
FragmentBuffer []byte
|
|
FragmentStartPktID uint16
|
|
FragmentCompressed bool
|
|
Fragmenting bool
|
|
|
|
// Server Data
|
|
Channels map[uint64]*Channel
|
|
|
|
// Audio
|
|
VoiceDecoders map[uint16]*opus.Decoder // Map VID (sender ID) to decoder
|
|
VoiceEncoder *opus.Encoder // Encoder for outgoing audio
|
|
VoiceEncoderMu sync.Mutex // Protects VoiceEncoder
|
|
|
|
// Event handler for public API
|
|
eventHandler EventHandler
|
|
|
|
// Done channel to signal shutdown
|
|
done chan 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),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// SetEventHandler sets the callback for events
|
|
func (c *Client) SetEventHandler(handler EventHandler) {
|
|
c.eventHandler = handler
|
|
}
|
|
|
|
// emitEvent sends an event to the handler if set
|
|
func (c *Client) emitEvent(eventType string, data map[string]any) {
|
|
if c.eventHandler != nil {
|
|
c.eventHandler(eventType, data)
|
|
}
|
|
}
|
|
|
|
// Stop signals the client to stop its loops
|
|
func (c *Client) Stop() {
|
|
select {
|
|
case <-c.done:
|
|
// Already closed
|
|
default:
|
|
close(c.done)
|
|
}
|
|
}
|
|
|
|
func (c *Client) Connect(address string) error {
|
|
conn, err := transport.NewTS3Conn(address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Conn = conn
|
|
log.Printf("Connected to UDP. Starting Handshake...")
|
|
|
|
// Initialize Handshake State
|
|
hs, err := NewHandshakeState(c.Conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Handshake = hs
|
|
|
|
// Improve Identity Security Level to 8 (Standard Requirement)
|
|
c.Handshake.ImproveSecurityLevel(8)
|
|
|
|
// Send Init1
|
|
if err := c.Handshake.SendPacket0(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Listen Loop
|
|
pktChan := c.Conn.PacketChan()
|
|
ticker := time.NewTicker(3 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
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
|
|
}
|
|
ping := protocol.NewPacket(protocol.PacketTypePing, nil)
|
|
c.PacketIDCounterC2S++
|
|
ping.Header.PacketID = c.PacketIDCounterC2S
|
|
ping.Header.ClientID = c.ClientID
|
|
// Must NOT have NewProtocol (0x20) flag for Pings/Pongs
|
|
ping.Header.Type = uint8(protocol.PacketTypePing) | protocol.PacketFlagUnencrypted
|
|
|
|
// Use SharedMac if available, otherwise zeros (as per ts3j InitPacketTransformation)
|
|
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
|
|
copy(ping.Header.MAC[:], c.Handshake.SharedMac)
|
|
} else {
|
|
// Initialize Header.MAC with zeros
|
|
for i := 0; i < 8; i++ {
|
|
ping.Header.MAC[i] = 0
|
|
}
|
|
}
|
|
|
|
log.Printf("Sending KeepAlive Ping (PID=%d)", ping.Header.PacketID)
|
|
c.Conn.SendPacket(ping)
|
|
}
|
|
}
|
|
}
|