feat: refactor client into reusable ts3client library
This commit is contained in:
@@ -17,6 +17,9 @@ type Channel struct {
|
||||
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
|
||||
@@ -29,7 +32,8 @@ type Client struct {
|
||||
VoicePacketID uint16
|
||||
|
||||
// State
|
||||
Connected bool
|
||||
Connected bool
|
||||
ServerName string
|
||||
|
||||
// Fragment reassembly
|
||||
FragmentBuffer []byte
|
||||
@@ -43,6 +47,12 @@ type Client struct {
|
||||
// Audio
|
||||
VoiceDecoders map[uint16]*opus.Decoder // Map VID (sender ID) to decoder
|
||||
VoiceEncoder *opus.Encoder // Encoder for outgoing audio
|
||||
|
||||
// Event handler for public API
|
||||
eventHandler EventHandler
|
||||
|
||||
// Done channel to signal shutdown
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewClient(nickname string) *Client {
|
||||
@@ -52,6 +62,29 @@ func NewClient(nickname string) *Client {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,11 +118,21 @@ func (c *Client) Connect(address string) error {
|
||||
|
||||
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
|
||||
|
||||
@@ -172,8 +172,13 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
log.Printf("Assigned ClientID: %d", c.ClientID)
|
||||
}
|
||||
if name, ok := args["virtualserver_name"]; ok {
|
||||
log.Printf("Server Name: %s", protocol.Unescape(name))
|
||||
c.ServerName = protocol.Unescape(name)
|
||||
log.Printf("Server Name: %s", c.ServerName)
|
||||
}
|
||||
c.emitEvent("connected", map[string]any{
|
||||
"clientID": c.ClientID,
|
||||
"serverName": c.ServerName,
|
||||
})
|
||||
case "channellist":
|
||||
// Parse channel info
|
||||
ch := &Channel{}
|
||||
@@ -193,13 +198,18 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
log.Printf("Channel: [%d] NameRaw=%q Order=%d Args=%v", ch.ID, ch.Name, ch.Order, args)
|
||||
case "channellistfinished":
|
||||
log.Printf("=== Channel List Complete (%d channels) ===", len(c.Channels))
|
||||
var channelList []*Channel
|
||||
var targetChan *Channel
|
||||
for _, ch := range c.Channels {
|
||||
log.Printf(" - [%d] %s (parent=%d)", ch.ID, ch.Name, ch.ParentID)
|
||||
channelList = append(channelList, ch)
|
||||
if ch.Name == "Test" {
|
||||
targetChan = ch
|
||||
}
|
||||
}
|
||||
c.emitEvent("channel_list", map[string]any{
|
||||
"channels": channelList,
|
||||
})
|
||||
|
||||
if targetChan == nil {
|
||||
if ch, ok := c.Channels[2]; ok {
|
||||
@@ -226,34 +236,65 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
case "notifycliententerview":
|
||||
// A client entered the server
|
||||
nick := ""
|
||||
clientID := uint16(0)
|
||||
channelID := uint64(0)
|
||||
if n, ok := args["client_nickname"]; ok {
|
||||
nick = protocol.Unescape(n)
|
||||
log.Printf("Client entered: %s", nick)
|
||||
}
|
||||
if cid, ok := args["clid"]; ok {
|
||||
var id uint64
|
||||
fmt.Sscanf(cid, "%d", &id)
|
||||
clientID = uint16(id)
|
||||
}
|
||||
if ctid, ok := args["ctid"]; ok {
|
||||
fmt.Sscanf(ctid, "%d", &channelID)
|
||||
}
|
||||
log.Printf("Client entered: %s (ID=%d)", nick, clientID)
|
||||
c.emitEvent("client_enter", map[string]any{
|
||||
"clientID": clientID,
|
||||
"nickname": nick,
|
||||
"channelID": channelID,
|
||||
})
|
||||
case "notifytextmessage":
|
||||
// targetmode: 1=Private, 2=Channel, 3=Server
|
||||
msg := ""
|
||||
invoker := "Unknown"
|
||||
var invokerID uint16
|
||||
var targetModeInt int
|
||||
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)
|
||||
}
|
||||
|
||||
targetMode := "Unknown"
|
||||
if tm, ok := args["targetmode"]; ok {
|
||||
switch tm {
|
||||
case "1":
|
||||
targetMode = "Private"
|
||||
targetModeInt = 1
|
||||
case "2":
|
||||
targetMode = "Channel"
|
||||
targetModeInt = 2
|
||||
case "3":
|
||||
targetMode = "Server"
|
||||
targetModeInt = 3
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[Chat][%s] %s: %s", targetMode, invoker, msg)
|
||||
c.emitEvent("message", map[string]any{
|
||||
"senderID": invokerID,
|
||||
"senderName": invoker,
|
||||
"message": msg,
|
||||
"targetMode": targetModeInt,
|
||||
})
|
||||
|
||||
case "notifyclientchatcomposing":
|
||||
// Someone is typing
|
||||
@@ -266,10 +307,21 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
|
||||
case "notifyclientmoved":
|
||||
// Client moved to another channel
|
||||
clid := args["clid"]
|
||||
ctid := args["ctid"]
|
||||
// reasonid: 0=switched, 1=moved, 2=timeout, 3=kick, 4=unknown
|
||||
log.Printf("Client %s moved to Channel %s", clid, ctid)
|
||||
var clientID uint16
|
||||
var channelID uint64
|
||||
if cid, ok := args["clid"]; ok {
|
||||
var id uint64
|
||||
fmt.Sscanf(cid, "%d", &id)
|
||||
clientID = uint16(id)
|
||||
}
|
||||
if ctid, ok := args["ctid"]; ok {
|
||||
fmt.Sscanf(ctid, "%d", &channelID)
|
||||
}
|
||||
log.Printf("Client %d moved to Channel %d", clientID, channelID)
|
||||
c.emitEvent("client_moved", map[string]any{
|
||||
"clientID": clientID,
|
||||
"channelID": channelID,
|
||||
})
|
||||
|
||||
case "notifyclientchannelgroupchanged":
|
||||
// Client channel group changed
|
||||
@@ -306,6 +358,40 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
|
||||
return c.SendCommand(cmd)
|
||||
|
||||
case "notifyclientleftview":
|
||||
// Client left the server
|
||||
var clientID uint16
|
||||
reason := ""
|
||||
if cid, ok := args["clid"]; ok {
|
||||
var id uint64
|
||||
fmt.Sscanf(cid, "%d", &id)
|
||||
clientID = uint16(id)
|
||||
}
|
||||
if rid, ok := args["reasonid"]; ok {
|
||||
switch rid {
|
||||
case "3":
|
||||
reason = "connection lost"
|
||||
case "5":
|
||||
reason = "kicked"
|
||||
case "6":
|
||||
reason = "banned"
|
||||
case "8":
|
||||
reason = "leaving"
|
||||
default:
|
||||
reason = "unknown"
|
||||
}
|
||||
}
|
||||
if rmsg, ok := args["reasonmsg"]; ok {
|
||||
if rmsg != "" {
|
||||
reason = protocol.Unescape(rmsg)
|
||||
}
|
||||
}
|
||||
log.Printf("Client %d left: %s", clientID, reason)
|
||||
c.emitEvent("client_left", map[string]any{
|
||||
"clientID": clientID,
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
case "notifyclientupdated":
|
||||
// Client updated (e.g. muted/unmuted)
|
||||
clid := args["clid"]
|
||||
@@ -316,6 +402,10 @@ func (c *Client) handleCommand(pkt *protocol.Packet) error {
|
||||
id := args["id"]
|
||||
msg := protocol.Unescape(args["msg"])
|
||||
log.Printf("SERVER ERROR: ID=%s MSG=%s", id, msg)
|
||||
c.emitEvent("error", map[string]any{
|
||||
"id": id,
|
||||
"message": msg,
|
||||
})
|
||||
|
||||
case "notifyservergrouplist", "notifychannelgrouplist", "notifyclientneededpermissions":
|
||||
// Ignore verbose noisy setup commands
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"go-ts/pkg/protocol"
|
||||
@@ -66,7 +67,7 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
||||
|
||||
// Only process Opus packets
|
||||
if codec != CodecOpusVoice && codec != CodecOpusMusic {
|
||||
log.Printf("Received non-Opus voice packet (Codec=%d). Ignoring echo.", codec)
|
||||
log.Printf("Received non-Opus voice packet (Codec=%d). Ignoring.", codec)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,26 +98,32 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
||||
}
|
||||
pcm = pcm[:n*channels]
|
||||
|
||||
if n != 960 {
|
||||
log.Printf("WARNING: Unusual Opus frame size: %d samples (expected 960 for 20ms)", n)
|
||||
// 3. Emit audio event instead of auto-echo
|
||||
c.emitEvent("audio", map[string]any{
|
||||
"senderID": vid,
|
||||
"codec": int(codec),
|
||||
"pcm": pcm,
|
||||
"channels": channels,
|
||||
})
|
||||
}
|
||||
|
||||
// SendVoice sends PCM audio data to the server
|
||||
// PCM must be 48kHz, 960 samples for 20ms frame (mono)
|
||||
func (c *Client) SendVoice(pcm []int16) error {
|
||||
if c.Conn == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
// 3. Process PCM: Reduce Volume (divide by 4)
|
||||
for i := range pcm {
|
||||
pcm[i] = pcm[i] / 10
|
||||
}
|
||||
channels := 1
|
||||
codec := uint8(CodecOpusVoice)
|
||||
|
||||
// 4. Get or Create Encoder
|
||||
// Get or Create Encoder
|
||||
if c.VoiceEncoder == nil {
|
||||
var err error
|
||||
app := opus.AppVoIP
|
||||
if channels == 2 {
|
||||
app = opus.AppAudio
|
||||
}
|
||||
encoder, err := opus.NewEncoder(48000, channels, app)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create Opus encoder: %v", err)
|
||||
return
|
||||
return fmt.Errorf("failed to create Opus encoder: %w", err)
|
||||
}
|
||||
|
||||
// Optimize Quality
|
||||
@@ -126,31 +133,28 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
||||
c.VoiceEncoder = encoder
|
||||
}
|
||||
|
||||
// 5. Encode PCM to Opus
|
||||
// Encode PCM to Opus
|
||||
encoded := make([]byte, 1024)
|
||||
nEnc, err := c.VoiceEncoder.Encode(pcm, encoded)
|
||||
if err != nil {
|
||||
log.Printf("Opus encode error: %v", err)
|
||||
return
|
||||
return fmt.Errorf("opus encode error: %w", err)
|
||||
}
|
||||
encoded = encoded[:nEnc]
|
||||
|
||||
// log.Printf("Voice Processed (CGO): VID=%d, In=%d bytes, PCM=%d samples, Out=%d bytes", vid, len(voiceData), n, nEnc)
|
||||
|
||||
// 6. Build echo packet (Client -> Server)
|
||||
// Build voice packet (Client -> Server)
|
||||
// Payload format: [VId(2)] [Codec(1)] [Data...]
|
||||
echoData := make([]byte, 2+1+len(encoded))
|
||||
voiceData := make([]byte, 2+1+len(encoded))
|
||||
|
||||
c.VoicePacketID++ // Increment counter before using it
|
||||
|
||||
// Correctly set VId in Payload to be the Sequence Number (not ClientID)
|
||||
binary.BigEndian.PutUint16(echoData[0:2], c.VoicePacketID)
|
||||
echoData[2] = codec
|
||||
copy(echoData[3:], encoded)
|
||||
// Set VId in Payload to be the Sequence Number (not ClientID)
|
||||
binary.BigEndian.PutUint16(voiceData[0:2], c.VoicePacketID)
|
||||
voiceData[2] = codec
|
||||
copy(voiceData[3:], encoded)
|
||||
|
||||
echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData)
|
||||
echoPkt.Header.PacketID = c.VoicePacketID
|
||||
echoPkt.Header.ClientID = c.ClientID
|
||||
pkt := protocol.NewPacket(protocol.PacketTypeVoice, voiceData)
|
||||
pkt.Header.PacketID = c.VoicePacketID
|
||||
pkt.Header.ClientID = c.ClientID
|
||||
|
||||
// Encrypt voice packet
|
||||
if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 {
|
||||
@@ -159,27 +163,26 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
||||
SharedMac: c.Handshake.SharedMac,
|
||||
GenerationID: 0,
|
||||
}
|
||||
key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true)
|
||||
key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true)
|
||||
|
||||
meta := make([]byte, 5)
|
||||
binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID)
|
||||
binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID)
|
||||
meta[4] = echoPkt.Header.Type
|
||||
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
|
||||
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
|
||||
meta[4] = pkt.Header.Type
|
||||
|
||||
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, echoPkt.Data)
|
||||
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
|
||||
if err != nil {
|
||||
log.Printf("Voice encryption failed: %v", err)
|
||||
return
|
||||
return fmt.Errorf("voice encryption failed: %w", err)
|
||||
}
|
||||
echoPkt.Data = encData
|
||||
copy(echoPkt.Header.MAC[:], mac)
|
||||
pkt.Data = encData
|
||||
copy(pkt.Header.MAC[:], mac)
|
||||
} else {
|
||||
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
|
||||
copy(echoPkt.Header.MAC[:], c.Handshake.SharedMac)
|
||||
copy(pkt.Header.MAC[:], c.Handshake.SharedMac)
|
||||
} else {
|
||||
echoPkt.Header.MAC = protocol.HandshakeMac
|
||||
pkt.Header.MAC = protocol.HandshakeMac
|
||||
}
|
||||
}
|
||||
|
||||
c.Conn.SendPacket(echoPkt)
|
||||
return c.Conn.SendPacket(pkt)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user