684 lines
24 KiB
Go
684 lines
24 KiB
Go
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)
|
|
}
|