feat: refactor client into reusable ts3client library
This commit is contained in:
93
cmd/example/main.go
Normal file
93
cmd/example/main.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"go-ts/pkg/ts3client"
|
||||
)
|
||||
|
||||
func main() {
|
||||
serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address")
|
||||
nickname := flag.String("nickname", "GoBot", "Nickname")
|
||||
flag.Parse()
|
||||
|
||||
log.Printf("=== TeamSpeak Library Example ===")
|
||||
log.Printf("Server: %s", *serverAddr)
|
||||
log.Printf("Nickname: %s", *nickname)
|
||||
|
||||
// Create client
|
||||
client := ts3client.New(*serverAddr, ts3client.Config{
|
||||
Nickname: *nickname,
|
||||
})
|
||||
|
||||
// Register event handlers
|
||||
client.On(ts3client.EventConnected, func(e *ts3client.ConnectedEvent) {
|
||||
log.Printf("✓ Connected! ClientID=%d, Server=%s", e.ClientID, e.ServerName)
|
||||
})
|
||||
|
||||
client.On(ts3client.EventChannelList, func(e *ts3client.ChannelListEvent) {
|
||||
log.Printf("✓ Received %d channels:", len(e.Channels))
|
||||
for _, ch := range e.Channels {
|
||||
log.Printf(" [%d] %s", ch.ID, ch.Name)
|
||||
}
|
||||
|
||||
// Join first non-default channel
|
||||
for _, ch := range e.Channels {
|
||||
if ch.ID != 1 && ch.ParentID == 0 {
|
||||
log.Printf("Joining channel: %s (ID=%d)", ch.Name, ch.ID)
|
||||
client.JoinChannel(ch.ID)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
client.On(ts3client.EventMessage, func(e *ts3client.MessageEvent) {
|
||||
log.Printf("[%s] %s: %s", e.TargetMode.String(), e.SenderName, e.Message)
|
||||
|
||||
// Echo back channel messages
|
||||
if e.TargetMode == ts3client.MessageTargetChannel {
|
||||
client.SendChannelMessage("Echo: " + e.Message)
|
||||
}
|
||||
})
|
||||
|
||||
client.On(ts3client.EventClientEnter, func(e *ts3client.ClientEnterEvent) {
|
||||
log.Printf("→ Client joined: %s (ID=%d)", e.Nickname, e.ClientID)
|
||||
})
|
||||
|
||||
client.On(ts3client.EventClientLeft, func(e *ts3client.ClientLeftEvent) {
|
||||
log.Printf("← Client left: ID=%d (%s)", e.ClientID, e.Reason)
|
||||
})
|
||||
|
||||
client.On(ts3client.EventAudio, func(e *ts3client.AudioEvent) {
|
||||
// Example: Echo audio (reduce volume)
|
||||
for i := range e.PCM {
|
||||
e.PCM[i] = e.PCM[i] / 4
|
||||
}
|
||||
client.SendAudio(e.PCM)
|
||||
})
|
||||
|
||||
client.On(ts3client.EventError, func(e *ts3client.ErrorEvent) {
|
||||
if e.ID != "0" { // ID 0 = OK
|
||||
log.Printf("! Server Error: [%s] %s", e.ID, e.Message)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle shutdown
|
||||
go func() {
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
log.Println("Shutting down...")
|
||||
client.Disconnect()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Connect (blocks until error or disconnect)
|
||||
if err := client.Connect(); err != nil {
|
||||
log.Fatalf("Connection error: %v", err)
|
||||
}
|
||||
}
|
||||
BIN
example.exe
Normal file
BIN
example.exe
Normal file
Binary file not shown.
@@ -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)
|
||||
}
|
||||
|
||||
368
pkg/ts3client/client.go
Normal file
368
pkg/ts3client/client.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package ts3client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-ts/internal/client"
|
||||
)
|
||||
|
||||
// Client is the main TeamSpeak 3 client
|
||||
type Client struct {
|
||||
address string
|
||||
config Config
|
||||
|
||||
// Internal client
|
||||
internal *client.Client
|
||||
|
||||
// Event handlers
|
||||
handlers map[EventType][]any
|
||||
mu sync.RWMutex
|
||||
|
||||
// State
|
||||
connected bool
|
||||
channels map[uint64]*Channel
|
||||
clients map[uint16]*ClientInfo
|
||||
serverInfo *ServerInfo
|
||||
selfInfo *SelfInfo
|
||||
channelsMu sync.RWMutex
|
||||
clientsMu sync.RWMutex
|
||||
}
|
||||
|
||||
// New creates a new TeamSpeak client
|
||||
func New(address string, config Config) *Client {
|
||||
// Apply defaults
|
||||
if config.Nickname == "" {
|
||||
config.Nickname = "GoTS3Bot"
|
||||
}
|
||||
if config.SecurityLevel == 0 {
|
||||
config.SecurityLevel = 8
|
||||
}
|
||||
if config.Version == "" {
|
||||
config.Version = "3.6.2 [Build: 1690976575]"
|
||||
}
|
||||
if config.Platform == "" {
|
||||
config.Platform = "Windows"
|
||||
}
|
||||
if config.HWID == "" {
|
||||
config.HWID = "1234567890"
|
||||
}
|
||||
|
||||
return &Client{
|
||||
address: address,
|
||||
config: config,
|
||||
handlers: make(map[EventType][]any),
|
||||
channels: make(map[uint64]*Channel),
|
||||
clients: make(map[uint16]*ClientInfo),
|
||||
}
|
||||
}
|
||||
|
||||
// On registers an event handler
|
||||
// The handler function signature must match the event type:
|
||||
// - EventConnected: func(*ConnectedEvent)
|
||||
// - EventMessage: func(*MessageEvent)
|
||||
// - EventAudio: func(*AudioEvent)
|
||||
// - etc.
|
||||
func (c *Client) On(event EventType, handler any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.handlers[event] = append(c.handlers[event], handler)
|
||||
}
|
||||
|
||||
// emit calls all handlers registered for the given event
|
||||
func (c *Client) emit(event EventType, data any) {
|
||||
c.mu.RLock()
|
||||
handlers := c.handlers[event]
|
||||
c.mu.RUnlock()
|
||||
|
||||
for _, h := range handlers {
|
||||
switch event {
|
||||
case EventConnected:
|
||||
if fn, ok := h.(func(*ConnectedEvent)); ok {
|
||||
fn(data.(*ConnectedEvent))
|
||||
}
|
||||
case EventDisconnected:
|
||||
if fn, ok := h.(func(*DisconnectedEvent)); ok {
|
||||
fn(data.(*DisconnectedEvent))
|
||||
}
|
||||
case EventMessage:
|
||||
if fn, ok := h.(func(*MessageEvent)); ok {
|
||||
fn(data.(*MessageEvent))
|
||||
}
|
||||
case EventClientEnter:
|
||||
if fn, ok := h.(func(*ClientEnterEvent)); ok {
|
||||
fn(data.(*ClientEnterEvent))
|
||||
}
|
||||
case EventClientLeft:
|
||||
if fn, ok := h.(func(*ClientLeftEvent)); ok {
|
||||
fn(data.(*ClientLeftEvent))
|
||||
}
|
||||
case EventClientMoved:
|
||||
if fn, ok := h.(func(*ClientMovedEvent)); ok {
|
||||
fn(data.(*ClientMovedEvent))
|
||||
}
|
||||
case EventChannelList:
|
||||
if fn, ok := h.(func(*ChannelListEvent)); ok {
|
||||
fn(data.(*ChannelListEvent))
|
||||
}
|
||||
case EventAudio:
|
||||
if fn, ok := h.(func(*AudioEvent)); ok {
|
||||
fn(data.(*AudioEvent))
|
||||
}
|
||||
case EventError:
|
||||
if fn, ok := h.(func(*ErrorEvent)); ok {
|
||||
fn(data.(*ErrorEvent))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes a connection to the TeamSpeak server
|
||||
// This method blocks until disconnected or an error occurs
|
||||
func (c *Client) Connect() error {
|
||||
c.internal = client.NewClient(c.config.Nickname)
|
||||
|
||||
// Set event callback on internal client
|
||||
c.internal.SetEventHandler(c.handleInternalEvent)
|
||||
|
||||
log.Printf("Connecting to %s as %s...", c.address, c.config.Nickname)
|
||||
|
||||
err := c.internal.Connect(c.address)
|
||||
if err != nil {
|
||||
c.emit(EventDisconnected, &DisconnectedEvent{Reason: err.Error()})
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectAsync connects in the background and returns immediately
|
||||
func (c *Client) ConnectAsync() <-chan error {
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
if err := c.Connect(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
close(errChan)
|
||||
}()
|
||||
// Give it a moment to start
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return errChan
|
||||
}
|
||||
|
||||
// Disconnect closes the connection gracefully
|
||||
func (c *Client) Disconnect() {
|
||||
if c.internal != nil {
|
||||
// Send disconnect command to server
|
||||
c.sendDisconnect("leaving")
|
||||
// Small delay to allow packet to be sent
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Stop the internal loop
|
||||
c.internal.Stop()
|
||||
if c.internal.Conn != nil {
|
||||
c.internal.Conn.Close()
|
||||
}
|
||||
}
|
||||
c.connected = false
|
||||
c.emit(EventDisconnected, &DisconnectedEvent{Reason: "client disconnect"})
|
||||
}
|
||||
|
||||
// sendDisconnect sends the disconnect command to the server
|
||||
func (c *Client) sendDisconnect(reason string) {
|
||||
if c.internal == nil {
|
||||
return
|
||||
}
|
||||
// Use internal client's SendCommand
|
||||
cmd := "clientdisconnect reasonid=8 reasonmsg=" + escapeTS3(reason)
|
||||
log.Printf("Sending disconnect: %s", cmd)
|
||||
if err := c.internal.SendCommandString(cmd); err != nil {
|
||||
log.Printf("Error sending disconnect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type disconnectCommand struct {
|
||||
reason string
|
||||
}
|
||||
|
||||
func (d *disconnectCommand) encode() string {
|
||||
return "clientdisconnect reasonid=8 reasonmsg=" + escapeTS3(d.reason)
|
||||
}
|
||||
|
||||
func escapeTS3(s string) string {
|
||||
// Basic escape for TS3 protocol
|
||||
result := ""
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '\\':
|
||||
result += "\\\\"
|
||||
case '/':
|
||||
result += "\\/"
|
||||
case ' ':
|
||||
result += "\\s"
|
||||
case '|':
|
||||
result += "\\p"
|
||||
default:
|
||||
result += string(r)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsConnected returns true if the client is connected
|
||||
func (c *Client) IsConnected() bool {
|
||||
return c.connected
|
||||
}
|
||||
|
||||
// handleInternalEvent processes events from the internal client
|
||||
func (c *Client) handleInternalEvent(eventType string, data map[string]any) {
|
||||
switch eventType {
|
||||
case "connected":
|
||||
c.connected = true
|
||||
clientID := uint16(0)
|
||||
serverName := ""
|
||||
if v, ok := data["clientID"].(uint16); ok {
|
||||
clientID = v
|
||||
}
|
||||
if v, ok := data["serverName"].(string); ok {
|
||||
serverName = v
|
||||
}
|
||||
c.selfInfo = &SelfInfo{ClientID: clientID, Nickname: c.config.Nickname}
|
||||
c.emit(EventConnected, &ConnectedEvent{
|
||||
ClientID: clientID,
|
||||
ServerName: serverName,
|
||||
})
|
||||
|
||||
case "message":
|
||||
targetMode := MessageTarget(1)
|
||||
if v, ok := data["targetMode"].(int); ok {
|
||||
targetMode = MessageTarget(v)
|
||||
}
|
||||
c.emit(EventMessage, &MessageEvent{
|
||||
SenderID: getUint16(data, "senderID"),
|
||||
SenderName: getString(data, "senderName"),
|
||||
Message: getString(data, "message"),
|
||||
TargetMode: targetMode,
|
||||
})
|
||||
|
||||
case "client_enter":
|
||||
info := &ClientInfo{
|
||||
ID: getUint16(data, "clientID"),
|
||||
Nickname: getString(data, "nickname"),
|
||||
ChannelID: getUint64(data, "channelID"),
|
||||
}
|
||||
c.clientsMu.Lock()
|
||||
c.clients[info.ID] = info
|
||||
c.clientsMu.Unlock()
|
||||
|
||||
c.emit(EventClientEnter, &ClientEnterEvent{
|
||||
ClientID: info.ID,
|
||||
Nickname: info.Nickname,
|
||||
ChannelID: info.ChannelID,
|
||||
})
|
||||
|
||||
case "client_left":
|
||||
clientID := getUint16(data, "clientID")
|
||||
c.clientsMu.Lock()
|
||||
delete(c.clients, clientID)
|
||||
c.clientsMu.Unlock()
|
||||
|
||||
c.emit(EventClientLeft, &ClientLeftEvent{
|
||||
ClientID: clientID,
|
||||
Reason: getString(data, "reason"),
|
||||
})
|
||||
|
||||
case "client_moved":
|
||||
c.emit(EventClientMoved, &ClientMovedEvent{
|
||||
ClientID: getUint16(data, "clientID"),
|
||||
ChannelID: getUint64(data, "channelID"),
|
||||
})
|
||||
|
||||
case "channel_list":
|
||||
if channels, ok := data["channels"].([]*client.Channel); ok {
|
||||
c.channelsMu.Lock()
|
||||
var chList []*Channel
|
||||
for _, ch := range channels {
|
||||
converted := &Channel{
|
||||
ID: ch.ID,
|
||||
ParentID: ch.ParentID,
|
||||
Name: ch.Name,
|
||||
Order: ch.Order,
|
||||
}
|
||||
c.channels[ch.ID] = converted
|
||||
chList = append(chList, converted)
|
||||
}
|
||||
c.channelsMu.Unlock()
|
||||
c.emit(EventChannelList, &ChannelListEvent{Channels: chList})
|
||||
}
|
||||
|
||||
case "audio":
|
||||
c.emit(EventAudio, &AudioEvent{
|
||||
SenderID: getUint16(data, "senderID"),
|
||||
Codec: AudioCodec(getInt(data, "codec")),
|
||||
PCM: getPCM(data, "pcm"),
|
||||
Channels: getInt(data, "channels"),
|
||||
})
|
||||
|
||||
case "error":
|
||||
c.emit(EventError, &ErrorEvent{
|
||||
ID: getString(data, "id"),
|
||||
Message: getString(data, "message"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for type conversion
|
||||
func getString(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getUint16(m map[string]any, key string) uint16 {
|
||||
if v, ok := m[key].(uint16); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := m[key].(int); ok {
|
||||
return uint16(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getUint64(m map[string]any, key string) uint64 {
|
||||
if v, ok := m[key].(uint64); ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := m[key].(int); ok {
|
||||
return uint64(v)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getInt(m map[string]any, key string) int {
|
||||
if v, ok := m[key].(int); ok {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getPCM(m map[string]any, key string) []int16 {
|
||||
if v, ok := m[key].([]int16); ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitForConnection waits until the client is connected or timeout
|
||||
func (c *Client) WaitForConnection(timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if c.connected {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return fmt.Errorf("connection timeout")
|
||||
}
|
||||
237
pkg/ts3client/commands.go
Normal file
237
pkg/ts3client/commands.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package ts3client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go-ts/pkg/protocol"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Channel Methods
|
||||
// =============================================================================
|
||||
|
||||
// GetChannels returns all known channels
|
||||
func (c *Client) GetChannels() []*Channel {
|
||||
c.channelsMu.RLock()
|
||||
defer c.channelsMu.RUnlock()
|
||||
|
||||
channels := make([]*Channel, 0, len(c.channels))
|
||||
for _, ch := range c.channels {
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// GetChannel returns a channel by ID
|
||||
func (c *Client) GetChannel(id uint64) *Channel {
|
||||
c.channelsMu.RLock()
|
||||
defer c.channelsMu.RUnlock()
|
||||
return c.channels[id]
|
||||
}
|
||||
|
||||
// GetCurrentChannel returns the client's current channel
|
||||
func (c *Client) GetCurrentChannel() *Channel {
|
||||
if c.selfInfo == nil {
|
||||
return nil
|
||||
}
|
||||
return c.GetChannel(c.selfInfo.ChannelID)
|
||||
}
|
||||
|
||||
// JoinChannel moves the client to the specified channel
|
||||
func (c *Client) JoinChannel(channelID uint64) error {
|
||||
return c.JoinChannelWithPassword(channelID, "")
|
||||
}
|
||||
|
||||
// JoinChannelWithPassword moves the client to a password-protected channel
|
||||
func (c *Client) JoinChannelWithPassword(channelID uint64, password string) error {
|
||||
if c.internal == nil || c.selfInfo == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientmove")
|
||||
cmd.AddParam("clid", fmt.Sprintf("%d", c.selfInfo.ClientID))
|
||||
cmd.AddParam("cid", fmt.Sprintf("%d", channelID))
|
||||
cmd.AddParam("cpw", password)
|
||||
|
||||
err := c.internal.SendCommand(cmd)
|
||||
if err == nil && c.selfInfo != nil {
|
||||
c.selfInfo.ChannelID = channelID
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Message Methods
|
||||
// =============================================================================
|
||||
|
||||
// SendChannelMessage sends a message to the current channel
|
||||
func (c *Client) SendChannelMessage(message string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("sendtextmessage")
|
||||
cmd.AddParam("targetmode", "2") // Channel
|
||||
cmd.AddParam("msg", message)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// SendPrivateMessage sends a private message to a specific client
|
||||
func (c *Client) SendPrivateMessage(clientID uint16, message string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("sendtextmessage")
|
||||
cmd.AddParam("targetmode", "1") // Private
|
||||
cmd.AddParam("target", fmt.Sprintf("%d", clientID))
|
||||
cmd.AddParam("msg", message)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// SendServerMessage sends a message to the entire server
|
||||
func (c *Client) SendServerMessage(message string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("sendtextmessage")
|
||||
cmd.AddParam("targetmode", "3") // Server
|
||||
cmd.AddParam("msg", message)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Audio Methods
|
||||
// =============================================================================
|
||||
|
||||
// SendAudio sends PCM audio data to the server
|
||||
// PCM must be 48kHz, mono (960 samples for 20ms frame)
|
||||
func (c *Client) SendAudio(pcm []int16) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
return c.internal.SendVoice(pcm)
|
||||
}
|
||||
|
||||
// SetInputMuted mutes or unmutes the microphone
|
||||
func (c *Client) SetInputMuted(muted bool) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
val := "0"
|
||||
if muted {
|
||||
val = "1"
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientupdate")
|
||||
cmd.AddParam("client_input_muted", val)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// SetOutputMuted mutes or unmutes the speaker
|
||||
func (c *Client) SetOutputMuted(muted bool) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
val := "0"
|
||||
if muted {
|
||||
val = "1"
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientupdate")
|
||||
cmd.AddParam("client_output_muted", val)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Client Methods
|
||||
// =============================================================================
|
||||
|
||||
// GetClients returns all connected clients
|
||||
func (c *Client) GetClients() []*ClientInfo {
|
||||
c.clientsMu.RLock()
|
||||
defer c.clientsMu.RUnlock()
|
||||
|
||||
clients := make([]*ClientInfo, 0, len(c.clients))
|
||||
for _, cl := range c.clients {
|
||||
clients = append(clients, cl)
|
||||
}
|
||||
return clients
|
||||
}
|
||||
|
||||
// GetClientByID returns a client by ID
|
||||
func (c *Client) GetClientByID(id uint16) *ClientInfo {
|
||||
c.clientsMu.RLock()
|
||||
defer c.clientsMu.RUnlock()
|
||||
return c.clients[id]
|
||||
}
|
||||
|
||||
// KickFromChannel kicks a client from their current channel
|
||||
func (c *Client) KickFromChannel(clientID uint16, reason string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientkick")
|
||||
cmd.AddParam("clid", fmt.Sprintf("%d", clientID))
|
||||
cmd.AddParam("reasonid", "4") // Kick from channel
|
||||
cmd.AddParam("reasonmsg", reason)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// KickFromServer kicks a client from the server
|
||||
func (c *Client) KickFromServer(clientID uint16, reason string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientkick")
|
||||
cmd.AddParam("clid", fmt.Sprintf("%d", clientID))
|
||||
cmd.AddParam("reasonid", "5") // Kick from server
|
||||
cmd.AddParam("reasonmsg", reason)
|
||||
|
||||
return c.internal.SendCommand(cmd)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Info Methods
|
||||
// =============================================================================
|
||||
|
||||
// GetServerInfo returns server information
|
||||
func (c *Client) GetServerInfo() *ServerInfo {
|
||||
return c.serverInfo
|
||||
}
|
||||
|
||||
// GetSelfInfo returns our own client information
|
||||
func (c *Client) GetSelfInfo() *SelfInfo {
|
||||
return c.selfInfo
|
||||
}
|
||||
|
||||
// SetNickname changes the client's nickname
|
||||
func (c *Client) SetNickname(name string) error {
|
||||
if c.internal == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
cmd := protocol.NewCommand("clientupdate")
|
||||
cmd.AddParam("client_nickname", name)
|
||||
|
||||
err := c.internal.SendCommand(cmd)
|
||||
if err == nil {
|
||||
c.config.Nickname = name
|
||||
if c.selfInfo != nil {
|
||||
c.selfInfo.Nickname = name
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
114
pkg/ts3client/events.go
Normal file
114
pkg/ts3client/events.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package ts3client
|
||||
|
||||
// EventType represents the type of event
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
// Connection events
|
||||
EventConnected EventType = "connected"
|
||||
EventDisconnected EventType = "disconnected"
|
||||
|
||||
// Message events
|
||||
EventMessage EventType = "message"
|
||||
|
||||
// Client events
|
||||
EventClientEnter EventType = "client_enter"
|
||||
EventClientLeft EventType = "client_left"
|
||||
EventClientMoved EventType = "client_moved"
|
||||
|
||||
// Channel events
|
||||
EventChannelList EventType = "channel_list"
|
||||
|
||||
// Audio events
|
||||
EventAudio EventType = "audio"
|
||||
|
||||
// Error events
|
||||
EventError EventType = "error"
|
||||
)
|
||||
|
||||
// ConnectedEvent is emitted when the client successfully connects
|
||||
type ConnectedEvent struct {
|
||||
ClientID uint16
|
||||
ServerName string
|
||||
}
|
||||
|
||||
// DisconnectedEvent is emitted when the client disconnects
|
||||
type DisconnectedEvent struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
// MessageEvent is emitted when a text message is received
|
||||
type MessageEvent struct {
|
||||
SenderID uint16
|
||||
SenderName string
|
||||
Message string
|
||||
TargetMode MessageTarget // Private, Channel, or Server
|
||||
}
|
||||
|
||||
// MessageTarget represents the target type of a message
|
||||
type MessageTarget int
|
||||
|
||||
const (
|
||||
MessageTargetPrivate MessageTarget = 1
|
||||
MessageTargetChannel MessageTarget = 2
|
||||
MessageTargetServer MessageTarget = 3
|
||||
)
|
||||
|
||||
func (m MessageTarget) String() string {
|
||||
switch m {
|
||||
case MessageTargetPrivate:
|
||||
return "Private"
|
||||
case MessageTargetChannel:
|
||||
return "Channel"
|
||||
case MessageTargetServer:
|
||||
return "Server"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ClientEnterEvent is emitted when a client enters the server
|
||||
type ClientEnterEvent struct {
|
||||
ClientID uint16
|
||||
Nickname string
|
||||
ChannelID uint64
|
||||
}
|
||||
|
||||
// ClientLeftEvent is emitted when a client leaves the server
|
||||
type ClientLeftEvent struct {
|
||||
ClientID uint16
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ClientMovedEvent is emitted when a client moves to a different channel
|
||||
type ClientMovedEvent struct {
|
||||
ClientID uint16
|
||||
ChannelID uint64
|
||||
}
|
||||
|
||||
// ChannelListEvent is emitted when the channel list is received
|
||||
type ChannelListEvent struct {
|
||||
Channels []*Channel
|
||||
}
|
||||
|
||||
// AudioEvent is emitted when voice data is received
|
||||
type AudioEvent struct {
|
||||
SenderID uint16
|
||||
Codec AudioCodec
|
||||
PCM []int16 // Decoded PCM data (48kHz, mono or stereo)
|
||||
Channels int // 1 for mono, 2 for stereo
|
||||
}
|
||||
|
||||
// AudioCodec represents the audio codec type
|
||||
type AudioCodec int
|
||||
|
||||
const (
|
||||
CodecOpusVoice AudioCodec = 4
|
||||
CodecOpusMusic AudioCodec = 5
|
||||
)
|
||||
|
||||
// ErrorEvent is emitted when the server reports an error
|
||||
type ErrorEvent struct {
|
||||
ID string
|
||||
Message string
|
||||
}
|
||||
54
pkg/ts3client/types.go
Normal file
54
pkg/ts3client/types.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package ts3client
|
||||
|
||||
// Channel represents a TeamSpeak channel
|
||||
type Channel struct {
|
||||
ID uint64
|
||||
ParentID uint64
|
||||
Name string
|
||||
Order uint64
|
||||
}
|
||||
|
||||
// Client represents a connected client
|
||||
type ClientInfo struct {
|
||||
ID uint16
|
||||
Nickname string
|
||||
ChannelID uint64
|
||||
}
|
||||
|
||||
// ServerInfo contains server information
|
||||
type ServerInfo struct {
|
||||
Name string
|
||||
WelcomeMessage string
|
||||
Platform string
|
||||
Version string
|
||||
MaxClients int
|
||||
ClientsOnline int
|
||||
ChannelsOnline int
|
||||
}
|
||||
|
||||
// SelfInfo contains our own client information
|
||||
type SelfInfo struct {
|
||||
ClientID uint16
|
||||
Nickname string
|
||||
ChannelID uint64
|
||||
}
|
||||
|
||||
// Config contains client configuration options
|
||||
type Config struct {
|
||||
Nickname string
|
||||
SecurityLevel int // Default: 8
|
||||
Version string // Default: "3.6.2 [Build: 1690976575]"
|
||||
Platform string // Default: "Windows"
|
||||
HWID string // Default: generated
|
||||
}
|
||||
|
||||
// DefaultConfig returns a Config with sensible defaults
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Nickname: "GoTS3Bot",
|
||||
SecurityLevel: 8,
|
||||
Version: "3.6.2 [Build: 1690976575]",
|
||||
Platform: "Windows",
|
||||
HWID: "1234567890",
|
||||
}
|
||||
}
|
||||
3
run.ps1
3
run.ps1
@@ -2,4 +2,5 @@ $env:PATH = "D:\esto_al_path\msys64\mingw64\bin;$env:PATH"
|
||||
$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/client/main.go --server localhost:9987
|
||||
go run ./cmd/example --server localhost:9987
|
||||
|
||||
Reference in New Issue
Block a user