首次推送
This commit is contained in:
@@ -0,0 +1,683 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user