package teamspeak import ( "context" "errors" "fmt" "log" "log/slog" "strconv" "strings" "time" "github.com/honeybbq/teamspeak-go/commands" ) var errNoDataReturnedForClient = errors.New("no data returned for client") // SendTextMessage sends a text message to a client, channel or server. func (c *Client) SendTextMessage(targetMode int, targetID uint64, message string) error { cmd := commands.BuildCommandOrdered("sendtextmessage", [][2]string{ {"targetmode", strconv.Itoa(targetMode)}, {"target", strconv.FormatUint(targetID, 10)}, {"msg", message}, }) return c.SendCommandNoWait(cmd) } // ClientMove moves a client to a different channel. func (c *Client) ClientMove(clid uint16, channelID uint64, password string) error { params := [][2]string{ {"clid", strconv.Itoa(int(clid))}, {"cid", strconv.FormatUint(channelID, 10)}, } if password != "" { params = append(params, [2]string{"cpw", prepareClientPassword(password)}) } cmd := commands.BuildCommandOrdered("clientmove", params) return c.ExecCommand(cmd, 10*time.Second) } // Poke sends a poke message to a client. func (c *Client) Poke(clid uint16, message string) error { cmd := commands.BuildCommandOrdered("clientpoke", [][2]string{ {"clid", strconv.Itoa(int(clid))}, {"msg", message}, }) return c.ExecCommand(cmd, 10*time.Second) } // SendVoice sends a raw Opus frame. Codec values: 4 = Opus voice, 5 = Opus music. func (c *Client) SendVoice(data []byte, codec byte) error { return c.handler.SendVoicePacket(data, codec) } // ClientID returns the client's own ID assigned by the server. func (c *Client) ClientID() uint16 { c.mu.Lock() defer c.mu.Unlock() return c.clid } // GetClientInfo fetches detailed information about a client. func (c *Client) GetClientInfo(clid uint16) (map[string]string, error) { cmd := fmt.Sprintf("clientinfo clid=%d", clid) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("%w: %d", errNoDataReturnedForClient, clid) } return data[0], nil } // ListChannels returns a list of all channels on the server. func (c *Client) ListChannels() ([]ChannelInfo, error) { data, err := c.ExecCommandWithResponse("channellist", 5*time.Second) if err != nil { log.Printf("[teamspeak-go] ListChannels ExecCommandWithResponse error: %v", err) return nil, err } log.Printf("[teamspeak-go] ListChannels got %d raw data rows", len(data)) for i, item := range data { log.Printf("[teamspeak-go] ListChannels row[%d]: %v", i, item) } channels := make([]ChannelInfo, 0, len(data)) for _, item := range data { cid, _ := parseUint64Value(item["cid"]) pid, _ := parseUint64Value(item["pid"]) name := item["channel_name"] channels = append(channels, ChannelInfo{ ID: cid, ParentID: pid, Name: commands.Unescape(name), }) } return channels, nil } // ListChannelsDetailed returns channels with extended properties. // 通过 channellist -topic -flags -voice -limits -icon 获取更丰富的频道属性。 func (c *Client) ListChannelsDetailed() ([]ChannelInfoDetailed, error) { data, err := c.ExecCommandWithResponse( "channellist -topic -flags -voice -limits -icon", 5*time.Second) if err != nil { return nil, err } channels := make([]ChannelInfoDetailed, 0, len(data)) for _, item := range data { cid, _ := parseUint64Value(item["cid"]) pid, _ := parseUint64Value(item["pid"]) order, _ := parseUint64Value(item["channel_order"]) name := item["channel_name"] topic := item["channel_topic"] codec, _ := parseIntValue(item["channel_codec"]) codecQuality, _ := parseIntValue(item["channel_codec_quality"]) neededTalkPower, _ := parseIntValue(item["channel_needed_talk_power"]) maxClients, _ := parseIntValue(item["channel_maxclients"]) maxFamilyClients, _ := parseIntValue(item["channel_maxfamilyclients"]) iconID, _ := parseInt64Value(item["channel_icon_id"]) neededModifyPower, _ := parseIntValue(item["channel_needed_modify_power"]) channels = append(channels, ChannelInfoDetailed{ ID: cid, ParentID: pid, Order: order, Name: commands.Unescape(name), Topic: commands.Unescape(topic), Codec: codec, CodecQuality: codecQuality, NeededTalkPower: neededTalkPower, MaxClients: maxClients, MaxFamilyClients: maxFamilyClients, IsMaxClientsUnlimited: parseBoolValue(item["channel_flag_maxclients_unlimited"]), IsMaxFamilyClientsUnlimited: parseBoolValue(item["channel_flag_maxfamilyclients_unlimited"]), IsOrdered: order > 0, IsPermanent: parseBoolValue(item["channel_flag_permanent"]), IsSemiPermanent: parseBoolValue(item["channel_flag_semi_permanent"]), IsDefault: parseBoolValue(item["channel_flag_default"]), IsPassword: parseBoolValue(item["channel_flag_password"]), HasPassword: parseBoolValue(item["channel_flag_password"]), NeededModifyPower: neededModifyPower, IconID: iconID, }) } return channels, nil } // ListClients returns a list of all clients currently connected to the server. func (c *Client) ListClients() ([]ClientInfo, error) { data, err := c.ExecCommandWithResponse("clientlist -uid -away -voice -groups", 5*time.Second) if err != nil { return nil, err } clients := make([]ClientInfo, 0, len(data)) for _, item := range data { clid, _ := parseUint16Value(item["clid"]) nick := item["client_nickname"] cid, _ := parseUint64Value(item["cid"]) uid := item["client_unique_identifier"] clientType, _ := strconv.Atoi(item["client_type"]) groupsStr := item["client_servergroups"] groups := make([]string, 0) if groupsStr != "" { groups = strings.Split(groupsStr, ",") } clients = append(clients, ClientInfo{ ID: clid, Nickname: commands.Unescape(nick), ChannelID: cid, UID: uid, Type: clientType, ServerGroups: groups, }) } return clients, nil } // WaitConnected waits for the connection handshake to be completed. // Returns nil on success, or the handshake/disconnect error on failure. func (c *Client) WaitConnected(ctx context.Context) error { c.logger.Info("WaitConnected: waiting for handshake to complete") select { case <-c.connectedChan: c.logger.Info("WaitConnected: channel unblocked", slog.Any("err", c.connectedErr)) return c.connectedErr case <-ctx.Done(): c.logger.Warn("WaitConnected: context done", slog.Any("err", ctx.Err())) return ctx.Err() } } // ─── 服务器查询 ──────────────────────────────────────────── // GetServerInfo returns server details. // 协议命令: serverinfo func (c *Client) GetServerInfo() (*ServerInfo, error) { data, err := c.ExecCommandWithResponse("serverinfo", 5*time.Second) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("serverinfo: %w", errNoDataReturnedForClient) } item := data[0] maxClients, _ := parseIntValue(item["virtualserver_maxclients"]) clientsOnline, _ := parseIntValue(item["virtualserver_clientsonline"]) channelsOnline, _ := parseIntValue(item["virtualserver_channelsonline"]) uptime, _ := parseInt64Value(item["virtualserver_uptime"]) created, _ := parseInt64Value(item["virtualserver_created"]) iconID, _ := parseInt64Value(item["virtualserver_icon_id"]) defServerGroup, _ := parseIntValue(item["virtualserver_default_server_group"]) defChannelGroup, _ := parseIntValue(item["virtualserver_default_channel_group"]) return &ServerInfo{ Name: commands.Unescape(item["virtualserver_name"]), WelcomeMessage: commands.Unescape(item["virtualserver_welcomemessage"]), MaxClients: maxClients, ClientsOnline: clientsOnline, ChannelsOnline: channelsOnline, Uptime: uptime, Version: item["virtualserver_version"], Platform: item["virtualserver_platform"], Created: created, IconID: iconID, DefaultServerGroup: defServerGroup, DefaultChannelGroup: defChannelGroup, }, nil } // ─── 频道查询 / 管理 ────────────────────────────────────── // GetChannelInfo returns detailed info for a single channel. // 协议命令: channelinfo cid=X // 与 ListChannelsDetailed 的区别:本方法返回单频道的完整信息(含 description), // ListChannelsDetailed 返回所有频道的部分属性。 func (c *Client) GetChannelInfo(channelID uint64) (*ChannelDetailInfo, error) { cmd := fmt.Sprintf("channelinfo cid=%d", channelID) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("channelinfo: %w", errNoDataReturnedForClient) } item := data[0] cid, _ := parseUint64Value(item["cid"]) pid, _ := parseUint64Value(item["channel_order"]) order, _ := parseUint64Value(item["channel_order"]) codec, _ := parseIntValue(item["channel_codec"]) codecQuality, _ := parseIntValue(item["channel_codec_quality"]) maxClients, _ := parseIntValue(item["channel_maxclients"]) maxFamilyClients, _ := parseIntValue(item["channel_maxfamilyclients"]) neededTalkPower, _ := parseIntValue(item["channel_needed_talk_power"]) iconID, _ := parseInt64Value(item["channel_icon_id"]) return &ChannelDetailInfo{ ID: cid, ParentID: pid, Order: order, Name: commands.Unescape(item["channel_name"]), Topic: commands.Unescape(item["channel_topic"]), Description: commands.Unescape(item["channel_description"]), Codec: codec, CodecQuality: codecQuality, MaxClients: maxClients, MaxFamilyClients: maxFamilyClients, NeededTalkPower: neededTalkPower, IconID: iconID, IsPermanent: parseBoolValue(item["channel_flag_permanent"]), IsSemiPermanent: parseBoolValue(item["channel_flag_semi_permanent"]), IsDefault: parseBoolValue(item["channel_flag_default"]), IsPassword: parseBoolValue(item["channel_flag_password"]), BannerGfxURL: item["channel_banner_gfx_url"], }, nil } // FindChannels searches channels by name pattern. // 协议命令: channelfind pattern=X func (c *Client) FindChannels(pattern string) ([]ChannelInfo, error) { cmd := commands.BuildCommand("channelfind", map[string]string{"pattern": pattern}) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } channels := make([]ChannelInfo, 0, len(data)) for _, item := range data { cid, _ := parseUint64Value(item["cid"]) channels = append(channels, ChannelInfo{ ID: cid, Name: commands.Unescape(item["channel_name"]), }) } return channels, nil } // CreateChannel creates a new channel. // 协议命令: channelcreate channel_name=X [params...] // 返回新建频道的 ID。 // 常用可选参数通过 options map 传递,例如: // - "channel_topic": "主题" // - "channel_flag_permanent": "1" // - "channel_password": "密码" // - "cpid": "父频道ID" func (c *Client) CreateChannel(name string, options map[string]string) (uint64, error) { params := [][2]string{{"channel_name", name}} for k, v := range options { params = append(params, [2]string{k, v}) } cmd := commands.BuildCommandOrdered("channelcreate", params) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return 0, err } if len(data) == 0 { return 0, fmt.Errorf("channelcreate: no cid returned") } cid, _ := parseUint64Value(data[0]["cid"]) return cid, nil } // EditChannel modifies channel properties. // 协议命令: channeledit cid=X [params...] func (c *Client) EditChannel(channelID uint64, properties map[string]string) error { params := [][2]string{{"cid", fmt.Sprintf("%d", channelID)}} for k, v := range properties { params = append(params, [2]string{k, v}) } cmd := commands.BuildCommandOrdered("channeledit", params) return c.ExecCommand(cmd, 5*time.Second) } // DeleteChannel deletes a channel. // 协议命令: channeldelete cid=X force=X // force=true 强制删除(含子频道)。 func (c *Client) DeleteChannel(channelID uint64, force bool) error { forceFlag := "0" if force { forceFlag = "1" } cmd := fmt.Sprintf("channeldelete cid=%d force=%s", channelID, forceFlag) return c.ExecCommand(cmd, 5*time.Second) } // MoveChannel moves a channel to a new parent or position. // 协议命令: channelmove cid=X cpid=X order=X func (c *Client) MoveChannel(channelID, parentID, order uint64) error { cmd := fmt.Sprintf("channelmove cid=%d cpid=%d order=%d", channelID, parentID, order) return c.ExecCommand(cmd, 5*time.Second) } // ─── 客户端查询 / 管理 ──────────────────────────────────── // GetClientDetailInfo returns extended client details. // 协议命令: clientinfo clid=X // 与 ListClients 返回的 ClientInfo 相比,本方法返回更丰富的属性 // (away 状态、平台、版本、IP、连接历史等)。 func (c *Client) GetClientDetailInfo(clid uint16) (*ClientDetailInfo, error) { cmd := fmt.Sprintf("clientinfo clid=%d", clid) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } if len(data) == 0 { return nil, fmt.Errorf("%w: %d", errNoDataReturnedForClient, clid) } item := data[0] cid, _ := parseUint16Value(item["clid"]) channelID, _ := parseUint64Value(item["cid"]) clientType, _ := parseIntValue(item["client_type"]) created, _ := parseInt64Value(item["client_created"]) lastConnected, _ := parseInt64Value(item["client_lastconnected"]) totalConnections, _ := parseIntValue(item["client_totalconnections"]) iconID, _ := parseInt64Value(item["client_icon_id"]) groupsStr := item["client_servergroups"] groups := make([]string, 0) if groupsStr != "" { groups = strings.Split(groupsStr, ",") } return &ClientDetailInfo{ ID: cid, Nickname: commands.Unescape(item["client_nickname"]), UID: item["client_unique_identifier"], ChannelID: channelID, Type: clientType, ServerGroups: groups, Away: parseBoolValue(item["client_away"]), AwayMessage: commands.Unescape(item["client_away_message"]), InputMuted: parseBoolValue(item["client_input_muted"]), OutputMuted: parseBoolValue(item["client_output_muted"]), Platform: item["client_platform"], Version: item["client_version"], IP: item["connection_client_ip"], Created: created, LastConnected: lastConnected, TotalConnections: totalConnections, Description: commands.Unescape(item["client_description"]), IconID: iconID, }, nil } // FindClientByDBID searches the client database by database ID. // 协议命令: clientdbfind pattern=X -uid // 返回客户端的 DBID 和 UID。 func (c *Client) FindClientByDBID(dbid uint64) (string, error) { cmd := fmt.Sprintf("clientdbfind -uid cldbid=%d", dbid) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return "", err } if len(data) == 0 { return "", fmt.Errorf("clientdbfind: no result for cldbid %d", dbid) } return data[0]["client_unique_identifier"], nil } // FindClientByName searches the client database by nickname. // 协议命令: clientdbfind pattern=X -uid func (c *Client) FindClientByName(nickname string) (uid string, dbid uint64, err error) { cmd := commands.BuildCommand("clientdbfind", map[string]string{ "pattern": nickname, }) + " -uid" data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return "", 0, err } if len(data) == 0 { return "", 0, fmt.Errorf("clientdbfind: no result for %q", nickname) } uid = data[0]["client_unique_identifier"] dbid, _ = parseUint64Value(data[0]["cldbid"]) return uid, dbid, nil } // ListDBClients returns registered clients from the server database. // 协议命令: clientdblist start=X duration=X // start: 起始位置, duration: 返回数量(0=全部) func (c *Client) ListDBClients(start, duration int) ([]DBClient, error) { cmd := fmt.Sprintf("clientdblist start=%d duration=%d", start, duration) data, err := c.ExecCommandWithResponse(cmd, 10*time.Second) if err != nil { return nil, err } clients := make([]DBClient, 0, len(data)) for _, item := range data { dbid, _ := parseUint64Value(item["cldbid"]) created, _ := parseInt64Value(item["client_created"]) lastConnected, _ := parseInt64Value(item["client_lastconnected"]) totalConnections, _ := parseIntValue(item["client_totalconnections"]) clients = append(clients, DBClient{ DBID: dbid, UID: item["client_unique_identifier"], Nickname: commands.Unescape(item["client_nickname"]), Created: created, LastConnected: lastConnected, TotalConnections: totalConnections, Description: commands.Unescape(item["client_description"]), }) } return clients, nil } // UpdateSelf updates the current client's properties. // 协议命令: clientupdate [params...] // 常用参数: // - client_nickname: 新昵称 // - client_away: "1"/"0" // - client_away_message: 离开消息 // - client_input_muted: "1"/"0" // - client_output_muted: "1"/"0" // - client_phonetic_nickname: 语音昵称 // - client_default_token: 默认 Token func (c *Client) UpdateSelf(properties map[string]string) error { if len(properties) == 0 { return nil } cmd := commands.BuildCommand("clientupdate", properties) return c.ExecCommand(cmd, 5*time.Second) } // KickClient kicks a client from channel or server. // 协议命令: clientkick clid=X reasonid=X reasonmsg=X // reasonid: 4=从频道踢出, 5=从服务器踢出 func (c *Client) KickClient(clid uint16, reasonID int, reasonMsg string) error { cmd := commands.BuildCommandOrdered("clientkick", [][2]string{ {"clid", fmt.Sprintf("%d", clid)}, {"reasonid", fmt.Sprintf("%d", reasonID)}, {"reasonmsg", reasonMsg}, }) return c.ExecCommand(cmd, 5*time.Second) } // ─── Ban 管理 ──────────────────────────────────────────── // ListBans returns all active ban entries. // 协议命令: banlist func (c *Client) ListBans() ([]BanEntry, error) { data, err := c.ExecCommandWithResponse("banlist", 5*time.Second) if err != nil { return nil, err } bans := make([]BanEntry, 0, len(data)) for _, item := range data { banID, _ := parseInt64Value(item["banid"]) created, _ := parseInt64Value(item["created"]) bans = append(bans, BanEntry{ BanID: banID, IP: item["ip"], Name: commands.Unescape(item["name"]), UID: item["uid"], Created: created, InvokerName: commands.Unescape(item["invokername"]), InvokerUID: item["invokeruid"], Reason: commands.Unescape(item["reason"]), Enforcement: parseBoolValue(item["enforcement"]), }) } return bans, nil } // AddBan adds a ban entry. // 协议命令: banadd ip=X name=X uid=X time=X banreason=X // ip/name/uid 至少指定一个;time 单位为秒,0=永久。 func (c *Client) AddBan(ip, name, uid string, timeSeconds int, reason string) error { params := make([][2]string, 0, 5) if ip != "" { params = append(params, [2]string{"ip", ip}) } if name != "" { params = append(params, [2]string{"name", name}) } if uid != "" { params = append(params, [2]string{"uid", uid}) } if timeSeconds > 0 { params = append(params, [2]string{"time", fmt.Sprintf("%d", timeSeconds)}) } if reason != "" { params = append(params, [2]string{"banreason", reason}) } cmd := commands.BuildCommandOrdered("banadd", params) return c.ExecCommand(cmd, 5*time.Second) } // DeleteBan removes a ban entry. // 协议命令: bandel banid=X func (c *Client) DeleteBan(banID int64) error { cmd := fmt.Sprintf("bandel banid=%d", banID) return c.ExecCommand(cmd, 5*time.Second) } // DeleteAllBans removes all ban entries. // 协议命令: bandelall func (c *Client) DeleteAllBans() error { return c.ExecCommand("bandelall", 5*time.Second) } // ─── 文件列表 ──────────────────────────────────────────── // ListFiles returns files and directories in a channel path. // 协议命令: ftgetfilelist cid=X path=X // path 为频道内虚拟路径,根目录为 "/"。 func (c *Client) ListFiles(channelID uint64, path string) ([]FileEntry, error) { cmd := commands.BuildCommandOrdered("ftgetfilelist", [][2]string{ {"cid", fmt.Sprintf("%d", channelID)}, {"path", path}, }) data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } files := make([]FileEntry, 0, len(data)) for _, item := range data { size, _ := parseUint64Value(item["size"]) dt, _ := parseInt64Value(item["datetime"]) files = append(files, FileEntry{ Name: commands.Unescape(item["name"]), Size: size, DateTime: dt, IsFile: parseBoolValue(item["is_file"]), }) } return files, nil } // ─── Token 管理 ────────────────────────────────────────── // ListTokens returns all privilege keys. // 协议命令: tokenlist func (c *Client) ListTokens() ([]TokenEntry, error) { data, err := c.ExecCommandWithResponse("tokenlist", 5*time.Second) if err != nil { return nil, err } tokens := make([]TokenEntry, 0, len(data)) for _, item := range data { tokenType, _ := parseIntValue(item["token_type"]) tokenID1, _ := parseInt64Value(item["token_id1"]) tokenID2, _ := parseInt64Value(item["token_id2"]) created, _ := parseInt64Value(item["token_created"]) tokens = append(tokens, TokenEntry{ Token: item["token"], TokenType: tokenType, TokenID1: tokenID1, TokenID2: tokenID2, Created: created, Description: commands.Unescape(item["token_description"]), }) } return tokens, nil } // UseToken activates a privilege key. // 协议命令: tokenuse token=X func (c *Client) UseToken(token string) error { cmd := commands.BuildCommand("tokenuse", map[string]string{"token": token}) return c.ExecCommand(cmd, 5*time.Second) } // ─── 投诉管理 ──────────────────────────────────────────── // ListComplaints returns complaints against a target client. // 协议命令: complainlist tcldbid=X // 若 targetDBID=0 则返回全部投诉。 func (c *Client) ListComplaints(targetDBID uint64) ([]ComplaintEntry, error) { cmd := "complainlist" if targetDBID > 0 { cmd = fmt.Sprintf("complainlist tcldbid=%d", targetDBID) } data, err := c.ExecCommandWithResponse(cmd, 5*time.Second) if err != nil { return nil, err } complaints := make([]ComplaintEntry, 0, len(data)) for _, item := range data { fromDBID, _ := parseUint64Value(item["fcldbid"]) toDBID, _ := parseUint64Value(item["tcldbid"]) ts, _ := parseInt64Value(item["timestamp"]) complaints = append(complaints, ComplaintEntry{ FromDBID: fromDBID, ToDBID: toDBID, Message: commands.Unescape(item["message"]), Timestamp: ts, }) } return complaints, nil } // AddComplaint files a complaint against a target client. // 协议命令: complainadd tcldbid=X message=X func (c *Client) AddComplaint(targetDBID uint64, message string) error { cmd := commands.BuildCommandOrdered("complainadd", [][2]string{ {"tcldbid", fmt.Sprintf("%d", targetDBID)}, {"message", message}, }) return c.ExecCommand(cmd, 5*time.Second) } // DeleteComplaint removes a complaint. // 协议命令: complaindel tcldbid=X fcldbid=X func (c *Client) DeleteComplaint(targetDBID, fromDBID uint64) error { cmd := fmt.Sprintf("complaindel tcldbid=%d fcldbid=%d", targetDBID, fromDBID) return c.ExecCommand(cmd, 5*time.Second) }