2026-01-15 22:06:35 +01:00
|
|
|
package ts3client
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-01-16 14:19:02 +01:00
|
|
|
"log"
|
|
|
|
|
"strings"
|
2026-01-15 22:06:35 +01:00
|
|
|
|
|
|
|
|
"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]
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-16 14:19:02 +01:00
|
|
|
// GetChannelByName returns the first channel matching the given name (case-insensitive substring match)
|
|
|
|
|
func (c *Client) GetChannelByName(name string) *Channel {
|
|
|
|
|
c.channelsMu.RLock()
|
|
|
|
|
defer c.channelsMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
// Debug: log all available channels
|
|
|
|
|
log.Printf("[GetChannelByName] Searching for: %q, Available channels (%d):", name, len(c.channels))
|
|
|
|
|
for id, ch := range c.channels {
|
|
|
|
|
log.Printf("[GetChannelByName] - [%d] %q", id, ch.Name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First try exact match
|
|
|
|
|
for _, ch := range c.channels {
|
|
|
|
|
if ch.Name == name {
|
|
|
|
|
return ch
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then try case-insensitive contains
|
|
|
|
|
nameLower := strings.ToLower(name)
|
|
|
|
|
for _, ch := range c.channels {
|
|
|
|
|
if strings.Contains(strings.ToLower(ch.Name), nameLower) {
|
|
|
|
|
return ch
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 22:06:35 +01:00
|
|
|
// 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))
|
2026-01-16 16:02:17 +01:00
|
|
|
if password != "" {
|
|
|
|
|
cmd.AddParam("cpw", password)
|
|
|
|
|
}
|
2026-01-15 22:06:35 +01:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|