929 lines
28 KiB
Go
929 lines
28 KiB
Go
package plugins
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
"trbot/utils"
|
||
"trbot/utils/configs"
|
||
"trbot/utils/consts"
|
||
"trbot/utils/flaterr"
|
||
"trbot/utils/handler_params"
|
||
"trbot/utils/plugin_utils"
|
||
"trbot/utils/type/contain"
|
||
"trbot/utils/type/message_utils"
|
||
"trbot/utils/yaml"
|
||
|
||
"github.com/go-telegram/bot"
|
||
"github.com/go-telegram/bot/models"
|
||
"github.com/multiplay/go-ts3"
|
||
"github.com/rs/zerolog"
|
||
)
|
||
|
||
var tsClient *ts3.Client
|
||
var tsErr error
|
||
|
||
var tsDataPath string = filepath.Join(consts.YAMLDataBaseDir, "teamspeak/", consts.YAMLFileName)
|
||
var botNickName string = "trbot_teamspeak_plugin"
|
||
|
||
var isCanReInit bool = true
|
||
var isSuccessInit bool = false
|
||
var isListening bool = false
|
||
var isCanListening bool = false
|
||
var isLoginFailed bool = false
|
||
|
||
var hasHandlerByChatID bool
|
||
|
||
var resetListenTicker = make(chan bool)
|
||
|
||
var tsData TSServerQuery
|
||
var privateOpts *handler_params.Message
|
||
|
||
var checkcount int
|
||
var isMessagePinned bool
|
||
var reconnectMessageID int
|
||
// var oldMessageIDs []int
|
||
|
||
type TSServerQuery struct {
|
||
// get Name And Password in TeamSpeak 3 Client -> `Tools` -> `ServerQuery Login`
|
||
URL string `yaml:"URL"`
|
||
Name string `yaml:"Name"`
|
||
Password string `yaml:"Password"`
|
||
GroupID int64 `yaml:"GroupID"`
|
||
PollingInterval int `yaml:"PollingInterval"`
|
||
SendMessageMode bool `yaml:"SendMessageMode"`
|
||
AutoDeleteMessage bool `yaml:"AutoDeleteMessage"`
|
||
DeleteTimeoutInMinute int `yaml:"DeleteTimeoutInMinute"`
|
||
PinMessageMode bool `yaml:"PinMessageMode"`
|
||
PinnedMessageID int `yaml:"PinnedMessageID"`
|
||
}
|
||
|
||
type OnlineClient struct {
|
||
Username string
|
||
JoinTime time.Time
|
||
}
|
||
|
||
|
||
func init() {
|
||
plugin_utils.AddInitializer(plugin_utils.Initializer{
|
||
Name: "teamspeak",
|
||
Func: func(ctx context.Context) error{
|
||
if initTeamSpeak(ctx) {
|
||
isSuccessInit = true
|
||
// 需要以群组 ID 来触发 handler 来获取 opts
|
||
plugin_utils.AddHandlerByChatIDHandlers(plugin_utils.ByChatIDHandler{
|
||
ForChatID: tsData.GroupID,
|
||
PluginName: "teamspeak_get_opts",
|
||
MessageHandler: getOptsHandler,
|
||
})
|
||
hasHandlerByChatID = true
|
||
}
|
||
return tsErr
|
||
},
|
||
})
|
||
|
||
plugin_utils.AddDataBaseHandler(plugin_utils.DatabaseHandler{
|
||
Name: "teamspeak",
|
||
Loader: readTeamspeakData,
|
||
Saver: saveTeamspeakData,
|
||
})
|
||
|
||
plugin_utils.AddHandlerHelpInfo(plugin_utils.HandlerHelp{
|
||
Name: "TeamSpeak 检测用户变动",
|
||
Description: "注意:使用此功能需要先在配置文件中手动填写配置文件\n\n使用 /ts3 命令随时查看服务器在线用户和监听状态,监听轮询时间为每 5 秒检测一次,若无法与服务器取得连接,将会自动尝试重新连接",
|
||
})
|
||
|
||
plugin_utils.AddSlashCommandHandlers(plugin_utils.SlashCommand{
|
||
SlashCommand: "ts3",
|
||
MessageHandler: showStatus,
|
||
})
|
||
|
||
plugin_utils.AddCallbackQueryHandlers(plugin_utils.CallbackQuery{
|
||
CallbackDataPrefix: "teamspeak",
|
||
CallbackQueryHandler: teamspeakCallbackHandler,
|
||
})
|
||
}
|
||
|
||
func initTeamSpeak(ctx context.Context) bool {
|
||
logger := zerolog.Ctx(ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "initTeamSpeak").
|
||
Logger()
|
||
|
||
// 读取配置文件
|
||
err := readTeamspeakData(ctx)
|
||
if err != nil {
|
||
tsErr = fmt.Errorf("failed to read teamspeak config data: %w", err)
|
||
isCanReInit = false
|
||
return false
|
||
}
|
||
|
||
// 如果服务器地址为空不允许重新启动
|
||
if tsData.URL == "" {
|
||
logger.Error().
|
||
Str("path", tsDataPath).
|
||
Msg("No URL in config")
|
||
tsErr = fmt.Errorf("no URL in config")
|
||
isCanReInit = false
|
||
return false
|
||
} else {
|
||
if tsClient != nil { tsClient.Close() }
|
||
tsClient, err = ts3.NewClient(tsData.URL)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to connect to server")
|
||
tsErr = fmt.Errorf("failed to connnect to server: %w", err)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// ServerQuery 账号名或密码为空也不允许重新启动
|
||
if tsData.Name == "" || tsData.Password == "" {
|
||
logger.Error().
|
||
Str("path", tsDataPath).
|
||
Msg("No Name/Password in config")
|
||
tsErr = fmt.Errorf("no Name/Password in config")
|
||
isCanReInit = false
|
||
return false
|
||
} else {
|
||
err = tsClient.Login(tsData.Name, tsData.Password)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to login to server")
|
||
tsErr = fmt.Errorf("failed to login to server: %w", err)
|
||
isLoginFailed = true
|
||
return false
|
||
} else {
|
||
isLoginFailed = false
|
||
}
|
||
}
|
||
|
||
// 检查要设定通知的群组 ID 是否存在
|
||
if tsData.GroupID == 0 {
|
||
logger.Error().
|
||
Str("path", tsDataPath).
|
||
Msg("No GroupID in config")
|
||
tsErr = fmt.Errorf("no GroupID in config")
|
||
isCanReInit = false
|
||
return false
|
||
}
|
||
|
||
// 显示服务端版本测试一下连接
|
||
v, err := tsClient.Version()
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to get server version")
|
||
tsErr = fmt.Errorf("failed to get server version: %w", err)
|
||
return false
|
||
} else {
|
||
logger.Info().
|
||
Str("version", v.Version).
|
||
Str("platform", v.Platform).
|
||
Int("build", v.Build).
|
||
Msg("TeamSpeak server connected")
|
||
}
|
||
|
||
// 切换默认虚拟服务器
|
||
err = tsClient.Use(1)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Msg("Failed to switch server")
|
||
tsErr = fmt.Errorf("failed to switch server: %w", err)
|
||
return false
|
||
}
|
||
|
||
// 改一下 bot 自己的 nickname,使得在检测用户列表时默认不显示自己
|
||
m, err := tsClient.Whoami()
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Msg("Failed to get bot info")
|
||
tsErr = fmt.Errorf("failed to get bot info: %w", err)
|
||
return false
|
||
} else if m != nil && m.ClientName != botNickName {
|
||
// 当 bot 自己的 nickname 不等于配置文件中的 nickname 时,才进行修改
|
||
err = tsClient.SetNick(botNickName)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Msg("Failed to set bot nickname")
|
||
tsErr = fmt.Errorf("failed to set nickname: %w", err)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 没遇到不可重新初始化的部分则返回初始化成功
|
||
return true
|
||
}
|
||
|
||
func readTeamspeakData(ctx context.Context) error {
|
||
logger := zerolog.Ctx(ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "ReadTeamspeakData").
|
||
Logger()
|
||
|
||
err := yaml.LoadYAML(tsDataPath, &tsData)
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
logger.Warn().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Not found teamspeak config file. Created new one")
|
||
err = yaml.SaveYAML(tsDataPath, &TSServerQuery{
|
||
PollingInterval: 5,
|
||
SendMessageMode: true,
|
||
DeleteTimeoutInMinute: 10,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to create empty config")
|
||
return fmt.Errorf("failed to create empty config: %w", err)
|
||
}
|
||
} else {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to read config file")
|
||
|
||
// 读取配置文件内容失败也不允许重新启动
|
||
return fmt.Errorf("failed to read config file: %w", err)
|
||
}
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
func saveTeamspeakData(ctx context.Context) error {
|
||
logger := zerolog.Ctx(ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "SaveTeamspeakData").
|
||
Logger()
|
||
|
||
err := yaml.SaveYAML(tsDataPath, &tsData)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", tsDataPath).
|
||
Msg("Failed to save teamspeak data")
|
||
return fmt.Errorf("failed to save teamspeak data: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 用于首次初始化成功时只要对应群组有任何消息,都能自动获取 privateOpts 用来定时发送消息,并开启监听协程
|
||
func getOptsHandler(opts *handler_params.Message) error {
|
||
logger := zerolog.Ctx(opts.Ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "getOptsHandler").
|
||
Logger()
|
||
|
||
if !isListening && isCanReInit && opts.Message.Chat.ID == tsData.GroupID {
|
||
privateOpts = opts
|
||
isCanListening = true
|
||
logger.Debug().Msg("success get opts by handler")
|
||
if !isLoginFailed {
|
||
go listenUserStatus(opts.Ctx)
|
||
logger.Debug().Msg("success start listen user status")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func showStatus(opts *handler_params.Message) error {
|
||
logger := zerolog.Ctx(opts.Ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "showStatus").
|
||
Logger()
|
||
|
||
var handlerErr flaterr.MultErr
|
||
|
||
// 如果首次初始化没成功,没有添加根据群组 ID 来触发的 handler,用户发送 /ts3 后可以通过这个来自动获取 opts 并启动监听
|
||
if !isListening && isCanReInit && opts.Message.Chat.ID == tsData.GroupID {
|
||
privateOpts = opts
|
||
isCanListening = true
|
||
logger.Debug().Msg("success get opts by showStatus")
|
||
if !isLoginFailed {
|
||
go listenUserStatus(opts.Ctx)
|
||
logger.Debug().Msg("success start listen user status")
|
||
}
|
||
}
|
||
|
||
var pendingMessage string
|
||
|
||
if isSuccessInit && isCanListening {
|
||
onlineClient, err := tsClient.Server.ClientList()
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Msg("Failed to get online client")
|
||
handlerErr.Addf("failed to get online client: %w", err)
|
||
pendingMessage = fmt.Sprintf("连接到 teamspeak 服务器发生错误:\n<blockquote expandable>%s</blockquote>", err)
|
||
} else {
|
||
pendingMessage += fmt.Sprintln("在线客户端:")
|
||
var userCount int
|
||
for _, client := range onlineClient {
|
||
if client.Nickname == botNickName {
|
||
// 统计用户数量时跳过此机器人
|
||
continue
|
||
}
|
||
pendingMessage += fmt.Sprintf("用户 [ %s ] ", client.Nickname)
|
||
userCount++
|
||
if client.OnlineClientExt != nil {
|
||
pendingMessage += fmt.Sprintf("在线时长 %d", *client.OnlineClientTimes.LastConnected)
|
||
}
|
||
pendingMessage += "\n"
|
||
}
|
||
if userCount == 0 {
|
||
pendingMessage = "当前无用户在线"
|
||
}
|
||
}
|
||
} else {
|
||
pendingMessage = fmt.Sprintf("初始化 teamspeak 插件时发生了一些错误:\n<blockquote expandable>%s</blockquote>\n\n", tsErr)
|
||
if isCanReInit {
|
||
if initTeamSpeak(opts.Ctx) {
|
||
isSuccessInit = true
|
||
if !isListening && !isLoginFailed && opts.Message.Chat.ID == tsData.GroupID {
|
||
go listenUserStatus(opts.Ctx)
|
||
logger.Debug().
|
||
Msg("Start listening user status")
|
||
}
|
||
if isListening {
|
||
resetListenTicker <- true
|
||
pendingMessage = "尝试重新初始化成功,现可正常运行"
|
||
} else {
|
||
pendingMessage = "当前用户列表不可用,正在等待对应群组中的消息..."
|
||
}
|
||
} else {
|
||
handlerErr.Addf("failed to reinit teamspeak plugin: %w", tsErr)
|
||
if isListening {
|
||
pendingMessage += "尝试重新初始化失败,您可以使用 /ts3 命令来尝试手动初始化,或等待自动重连"
|
||
} else {
|
||
pendingMessage += "尝试重新初始化失败,您需要在服务器在线时手动使用 /ts3 命令来尝试初始化"
|
||
}
|
||
}
|
||
} else {
|
||
pendingMessage += "这是一个无法恢复的错误,您可能需要联系机器人管理员"
|
||
}
|
||
}
|
||
|
||
var buttons models.ReplyMarkup
|
||
if opts.Message.Chat.ID == tsData.GroupID && contain.Int64(opts.Message.From.ID, utils.GetChatAdminIDs(opts.Ctx, opts.Thebot, tsData.GroupID)...) || contain.Int64(opts.Message.From.ID, configs.BotConfig.AdminIDs...) {
|
||
buttons = &models.InlineKeyboardMarkup{ InlineKeyboard: [][]models.InlineKeyboardButton{{{
|
||
Text: "管理此功能",
|
||
CallbackData: "teamspeak",
|
||
}}}}
|
||
}
|
||
|
||
_, err := opts.Thebot.SendMessage(opts.Ctx, &bot.SendMessageParams{
|
||
ChatID: opts.Message.Chat.ID,
|
||
Text: pendingMessage,
|
||
ParseMode: models.ParseModeHTML,
|
||
ReplyParameters: &models.ReplyParameters{ MessageID: opts.Message.ID },
|
||
ReplyMarkup: buttons,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", opts.Message.Chat.ID).
|
||
Str("content", "teamspeak online client status").
|
||
Msg(flaterr.SendMessage.Str())
|
||
handlerErr.Addt(flaterr.SendMessage, "teamspeak online client status", err)
|
||
}
|
||
|
||
return handlerErr.Flat()
|
||
}
|
||
|
||
func listenUserStatus(ctx context.Context) {
|
||
logger := zerolog.Ctx(ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "listenUserStatus").
|
||
Logger()
|
||
|
||
isListening = true
|
||
if tsData.PollingInterval == 0 { tsData.PollingInterval = 5 }
|
||
listenTicker := time.NewTicker(time.Second * time.Duration(tsData.PollingInterval))
|
||
defer listenTicker.Stop()
|
||
|
||
// if tsData.DeleteTimeoutInMinute == 0 { tsData.DeleteTimeoutInMinute = 10 }
|
||
// deleteMessageTicker := time.NewTicker(time.Second * time.Duration(tsData.DeleteTimeoutInMinute))
|
||
// defer deleteMessageTicker.Stop()
|
||
|
||
if hasHandlerByChatID {
|
||
hasHandlerByChatID = false
|
||
// 获取到 privateOpts 后删掉 handler by chatID
|
||
plugin_utils.RemoveHandlerByChatIDHandler(tsData.GroupID, "teamspeak_get_opts")
|
||
}
|
||
|
||
// 取消置顶上一次的置顶消息
|
||
if tsData.PinnedMessageID != 0 {
|
||
_, err := privateOpts.Thebot.UnpinChatMessage(ctx, &bot.UnpinChatMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageID: tsData.PinnedMessageID,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Int("messageID", tsData.PinnedMessageID).
|
||
Str("content", "latest pinned online client status").
|
||
Msg(flaterr.UnpinChatMessage.Str())
|
||
}
|
||
tsData.PinnedMessageID = 0
|
||
}
|
||
|
||
var retryCount int
|
||
var checkFailedCount int
|
||
var beforeOnlineClient []OnlineClient
|
||
|
||
for {
|
||
select {
|
||
case <-resetListenTicker:
|
||
listenTicker.Reset(time.Second * time.Duration(tsData.PollingInterval))
|
||
isCanListening = true
|
||
retryCount = 0
|
||
// case <-deleteMessageTicker.C:
|
||
case <-listenTicker.C:
|
||
if isSuccessInit && isCanListening {
|
||
beforeOnlineClient = checkOnlineClientChange(ctx, &checkFailedCount, beforeOnlineClient)
|
||
} else {
|
||
logger.Info().Msg("try reconnect...")
|
||
// 出现错误时,先降低 ticker 速度,然后尝试重新初始化
|
||
if retryCount < 15 { retryCount++ }
|
||
listenTicker.Reset(time.Duration(retryCount) * 20 * time.Second)
|
||
if initTeamSpeak(ctx) {
|
||
isSuccessInit = true
|
||
isCanListening = true
|
||
// 重新初始化成功则恢复 ticker 速度
|
||
retryCount = 1
|
||
listenTicker.Reset(time.Second * time.Duration(tsData.PollingInterval))
|
||
logger.Info().Msg("reconnect success")
|
||
botMessage, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
Text: "已成功与服务器重新建立连接",
|
||
ParseMode: models.ParseModeHTML,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "success reconnect to server notice").
|
||
Msg(flaterr.SendMessage.Str())
|
||
} else {
|
||
time.Sleep(time.Second * 3)
|
||
var deleteMessageIDs []int = []int{botMessage.ID}
|
||
if reconnectMessageID != 0 {
|
||
deleteMessageIDs = []int{botMessage.ID, reconnectMessageID}
|
||
reconnectMessageID = 0
|
||
}
|
||
_, err = privateOpts.Thebot.DeleteMessages(privateOpts.Ctx, &bot.DeleteMessagesParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageIDs: deleteMessageIDs,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Int("messageID", botMessage.ID).
|
||
Str("content", "success reconnect to server notice").
|
||
Msg(flaterr.DeleteMessages.Str())
|
||
}
|
||
}
|
||
} else {
|
||
// 无法成功则等待下一个周期继续尝试
|
||
logger.Warn().
|
||
Err(tsErr).
|
||
Int("retryCount", retryCount).
|
||
Int("nextRetry", (retryCount) * 20).
|
||
Msg("connect failed")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func checkOnlineClientChange(ctx context.Context, count *int, before []OnlineClient) []OnlineClient {
|
||
var nowOnlineClient []OnlineClient
|
||
logger := zerolog.Ctx(ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "checkOnlineClientChange").
|
||
Logger()
|
||
|
||
onlineClient, err := tsClient.Server.ClientList()
|
||
if err != nil {
|
||
*count++
|
||
logger.Error().
|
||
Err(err).
|
||
Int("failedCount", *count).
|
||
Msg("Failed to get online client")
|
||
if err.Error() == "not connected" {
|
||
*count = 0
|
||
isCanListening = false
|
||
}
|
||
if *count >= 5 {
|
||
*count = 0
|
||
isCanListening = false
|
||
botMessage, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
Text: "已连续五次检查在线客户端失败,开始尝试自动重连",
|
||
ParseMode: models.ParseModeHTML,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "failed to check online client 5 times, start auto reconnect").
|
||
Msg(flaterr.SendMessage.Str())
|
||
} else {
|
||
reconnectMessageID = botMessage.ID
|
||
}
|
||
}
|
||
return before
|
||
} else {
|
||
for _, client := range onlineClient {
|
||
if client.Nickname == botNickName { continue }
|
||
var isExist bool
|
||
for _, user := range before {
|
||
if user.Username == client.Nickname {
|
||
nowOnlineClient = append(nowOnlineClient, user)
|
||
isExist = true
|
||
}
|
||
}
|
||
if !isExist {
|
||
nowOnlineClient = append(nowOnlineClient, OnlineClient{
|
||
Username: client.Nickname,
|
||
JoinTime: time.Now(),
|
||
})
|
||
}
|
||
}
|
||
added, removed := diffSlices(before, nowOnlineClient)
|
||
if tsData.SendMessageMode && len(added) + len(removed) > 0 {
|
||
logger.Debug().
|
||
Int("clientJoin", len(added)).
|
||
Int("clientLeave", len(removed)).
|
||
Msg("online client change detected")
|
||
notifyClientChange(added, removed)
|
||
}
|
||
if tsData.PinMessageMode {
|
||
if tsData.PinnedMessageID == 0 {
|
||
message, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
Text: "开始监听 Teamspeak 3 用户状态",
|
||
ParseMode: models.ParseModeHTML,
|
||
DisableNotification: true,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "start listen teamspeak user changes").
|
||
Msg(flaterr.SendMessage.Str())
|
||
return nowOnlineClient
|
||
}
|
||
tsData.PinnedMessageID = message.ID
|
||
err = yaml.SaveYAML(tsDataPath, &tsData)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("path", KeywordDataPath).
|
||
Msg("Failed to save teamspeak data")
|
||
} else {
|
||
// 置顶消息提醒
|
||
ok, err := privateOpts.Thebot.PinChatMessage(privateOpts.Ctx, &bot.PinChatMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageID: tsData.PinnedMessageID,
|
||
DisableNotification: true,
|
||
})
|
||
if ok {
|
||
isMessagePinned = true
|
||
// 删除置顶消息提示
|
||
plugin_utils.AddHandlerByMessageTypeHandlers(plugin_utils.ByMessageTypeHandler{
|
||
PluginName: "remove pin message notice",
|
||
ChatType: privateOpts.Message.Chat.Type,
|
||
MessageType: message_utils.PinnedMessage,
|
||
ForChatID: tsData.GroupID,
|
||
AllowAutoTrigger: true,
|
||
MessageHandler: func(opts *handler_params.Message) error {
|
||
if opts.Message.PinnedMessage != nil {
|
||
if opts.Message.PinnedMessage.Message.ID == tsData.PinnedMessageID {
|
||
_, err := opts.Thebot.DeleteMessage(opts.Ctx, &bot.DeleteMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageID: opts.Message.ID,
|
||
})
|
||
// 不管成功与否,都注销这个 handler
|
||
plugin_utils.RemoveHandlerByMessageTypeHandler(models.ChatTypeSupergroup, message_utils.PinnedMessage, tsData.GroupID, "remove pin message notice")
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
},
|
||
})
|
||
} else {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "listen teamspeak user changes").
|
||
Msg(flaterr.PinChatMessage.Str())
|
||
}
|
||
}
|
||
}
|
||
changePinnedMessage(nowOnlineClient, added, removed)
|
||
}
|
||
return nowOnlineClient
|
||
}
|
||
}
|
||
|
||
func diffSlices(before, now []OnlineClient) (added, removed []string) {
|
||
beforeMap := make(map[string]bool)
|
||
nowMap := make(map[string]bool)
|
||
|
||
// 把 A 和 B 转成 map
|
||
for _, item := range before { beforeMap[item.Username] = true }
|
||
for _, item := range now { nowMap[item.Username] = true }
|
||
|
||
// 删除
|
||
for item := range nowMap {
|
||
if !beforeMap[item] { added = append(added, item)}
|
||
}
|
||
|
||
// 新增
|
||
for item := range beforeMap {
|
||
if !nowMap[item] { removed = append(removed, item) }
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func notifyClientChange(add, remove []string) {
|
||
var pendingMessage string
|
||
logger := zerolog.Ctx(privateOpts.Ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "notifyClientChange").
|
||
Logger()
|
||
|
||
if len(add) > 0 {
|
||
pendingMessage += fmt.Sprintln("以下用户进入了服务器:")
|
||
for _, nickname := range add {
|
||
pendingMessage += fmt.Sprintf("用户 [ %s ]\n", nickname)
|
||
}
|
||
}
|
||
if len(remove) > 0 {
|
||
pendingMessage += fmt.Sprintln("以下用户离开了服务器:")
|
||
for _, nickname := range remove {
|
||
pendingMessage += fmt.Sprintf("用户 [ %s ]\n", nickname)
|
||
}
|
||
}
|
||
|
||
msg, err := privateOpts.Thebot.SendMessage(privateOpts.Ctx, &bot.SendMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
Text: pendingMessage,
|
||
ParseMode: models.ParseModeHTML,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "teamspeak user change notify").
|
||
Msg(flaterr.SendMessage.Str())
|
||
} else {
|
||
// oldMessageIDs = append(oldMessageIDs, msg.ID)
|
||
go deleteOldMessage(msg.ID)
|
||
}
|
||
}
|
||
|
||
func changePinnedMessage(online []OnlineClient, add, remove []string) {
|
||
logger := zerolog.Ctx(privateOpts.Ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "changePinnedMessage").
|
||
Logger()
|
||
|
||
// var checkcount int static
|
||
|
||
// 没有新加入和离开用户,等待一阵子后再更新用户在线时间
|
||
if len(add) + len(remove) == 0 && checkcount < 12 {
|
||
checkcount++
|
||
return
|
||
} else {
|
||
checkcount = 0
|
||
}
|
||
|
||
var pendingMessage string = fmt.Sprintf("%s | ", time.Now().Format("15:04"))
|
||
|
||
if len(online) > 0 {
|
||
pendingMessage += fmt.Sprintf("有 %d 位用户在线:\n<blockquote expandable>", len(online))
|
||
for _, client := range online {
|
||
pendingMessage += fmt.Sprintf("[ %s ] 已在线 %.1f 分钟\n", client.Username, time.Since(client.JoinTime).Minutes())
|
||
}
|
||
pendingMessage += "</blockquote>\n"
|
||
} else {
|
||
pendingMessage += "没有用户在线\n\n"
|
||
}
|
||
|
||
if len(add) + len(remove) > 0 {
|
||
if len(add) > 0 {
|
||
pendingMessage += fmt.Sprintln("以下用户进入了服务器:")
|
||
for _, nickname := range add {
|
||
pendingMessage += fmt.Sprintf("用户 [ %s ]\n", nickname)
|
||
}
|
||
}
|
||
if len(remove) > 0 {
|
||
pendingMessage += fmt.Sprintln("以下用户离开了服务器:")
|
||
for _, nickname := range remove {
|
||
pendingMessage += fmt.Sprintf("用户 [ %s ]\n", nickname)
|
||
}
|
||
}
|
||
}
|
||
|
||
if !isMessagePinned {
|
||
pendingMessage += "<blockquote expandable>无法置顶用户列表消息,请检查机器人是否拥有对应的权限,您也可以手动置顶此消息</blockquote>"
|
||
}
|
||
|
||
_, err := privateOpts.Thebot.EditMessageText(privateOpts.Ctx, &bot.EditMessageTextParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageID: tsData.PinnedMessageID,
|
||
Text: pendingMessage,
|
||
ParseMode: models.ParseModeHTML,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Int64("chatID", tsData.GroupID).
|
||
Str("content", "teamspeak user change notify").
|
||
Msg(flaterr.EditMessageText.Str())
|
||
}
|
||
}
|
||
|
||
func teamspeakCallbackHandler(opts *handler_params.CallbackQuery) error {
|
||
logger := zerolog.Ctx(opts.Ctx).
|
||
With().
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "teamspeakCallbackHandler").
|
||
Str("callbackQueryData", opts.CallbackQuery.Data).
|
||
Logger()
|
||
|
||
var handlerErr flaterr.MultErr
|
||
|
||
if contain.Int64(opts.CallbackQuery.From.ID, utils.GetChatAdminIDs(opts.Ctx, opts.Thebot, tsData.GroupID)...) || contain.Int64(opts.CallbackQuery.From.ID, configs.BotConfig.AdminIDs...) {
|
||
var needEdit bool = true
|
||
var needSave bool = false
|
||
|
||
switch opts.CallbackQuery.Data {
|
||
case "teamspeak_pinmessage":
|
||
if tsData.PinMessageMode && !tsData.SendMessageMode {
|
||
needEdit = false
|
||
_, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
|
||
CallbackQueryID: opts.CallbackQuery.ID,
|
||
Text: "您至少要保留一个检测用户变动的方式",
|
||
ShowAlert: true,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("content", "at least need one notice method").
|
||
Msg(flaterr.AnswerCallbackQuery.Str())
|
||
handlerErr.Addt(flaterr.AnswerCallbackQuery, "at least need one notice method", err)
|
||
}
|
||
} else {
|
||
tsData.PinMessageMode = !tsData.PinMessageMode
|
||
needSave = true
|
||
}
|
||
case "teamspeak_sendmessage":
|
||
if tsData.SendMessageMode && !tsData.PinMessageMode {
|
||
needEdit = false
|
||
_, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
|
||
CallbackQueryID: opts.CallbackQuery.ID,
|
||
Text: "您至少要保留一个检测用户变动的方式",
|
||
ShowAlert: true,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("content", "at least need one notice method").
|
||
Msg(flaterr.AnswerCallbackQuery.Str())
|
||
handlerErr.Addt(flaterr.AnswerCallbackQuery, "at least need one notice method", err)
|
||
}
|
||
} else {
|
||
tsData.SendMessageMode = !tsData.SendMessageMode
|
||
needSave = true
|
||
}
|
||
case "teamspeak_sendmessage_autodelete":
|
||
tsData.AutoDeleteMessage = !tsData.AutoDeleteMessage
|
||
needSave = true
|
||
}
|
||
|
||
if needEdit {
|
||
_, err := opts.Thebot.EditMessageText(opts.Ctx, &bot.EditMessageTextParams{
|
||
ChatID: opts.CallbackQuery.Message.Message.Chat.ID,
|
||
MessageID: opts.CallbackQuery.Message.Message.ID,
|
||
Text: "选择通知用户变动的方式",
|
||
ReplyMarkup: buildTeamspeakConfigKeyboard(),
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("content", "teamspeak manage keyboard").
|
||
Msg(flaterr.EditMessageText.Str())
|
||
handlerErr.Addt(flaterr.EditMessageText, "teamspeak manage keyboard", err)
|
||
}
|
||
}
|
||
|
||
if needSave {
|
||
err := saveTeamspeakData(opts.Ctx)
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Msg("Failed to save teamspeak data")
|
||
handlerErr.Addf("failed to save teamspeak data: %w", err)
|
||
}
|
||
}
|
||
} else {
|
||
_, err := opts.Thebot.AnswerCallbackQuery(opts.Ctx, &bot.AnswerCallbackQueryParams{
|
||
CallbackQueryID: opts.CallbackQuery.ID,
|
||
Text: "您没有权限修改此内容",
|
||
ShowAlert: true,
|
||
})
|
||
if err != nil {
|
||
logger.Error().
|
||
Err(err).
|
||
Str("content", "no permission to change teamspeak config").
|
||
Msg(flaterr.AnswerCallbackQuery.Str())
|
||
handlerErr.Addt(flaterr.AnswerCallbackQuery, "no permission to change teamspeak config", err)
|
||
}
|
||
}
|
||
|
||
return handlerErr.Flat()
|
||
}
|
||
|
||
func buildTeamspeakConfigKeyboard() models.ReplyMarkup {
|
||
var buttons [][]models.InlineKeyboardButton
|
||
|
||
if tsData.SendMessageMode {
|
||
buttons = append(buttons, []models.InlineKeyboardButton{
|
||
{
|
||
Text: utils.TextForTrueOrFalse(tsData.SendMessageMode, "✅ ", "") + "发送消息通知",
|
||
CallbackData: "teamspeak_sendmessage",
|
||
},
|
||
{
|
||
Text: utils.TextForTrueOrFalse(tsData.AutoDeleteMessage, "✅ ", "") + "自动删除旧消息",
|
||
CallbackData: "teamspeak_sendmessage_autodelete",
|
||
},
|
||
})
|
||
} else {
|
||
buttons = append(buttons, []models.InlineKeyboardButton{{
|
||
Text: utils.TextForTrueOrFalse(tsData.SendMessageMode, "✅ ", "") + "发送消息通知",
|
||
CallbackData: "teamspeak_sendmessage",
|
||
}})
|
||
}
|
||
|
||
buttons = append(buttons, []models.InlineKeyboardButton{{
|
||
Text: utils.TextForTrueOrFalse(tsData.PinMessageMode, "✅", "") + "显示在置顶消息",
|
||
CallbackData: "teamspeak_pinmessage",
|
||
}})
|
||
buttons = append(buttons, []models.InlineKeyboardButton{{
|
||
Text: "关闭菜单",
|
||
CallbackData: "delete_this_message",
|
||
}},)
|
||
|
||
return &models.InlineKeyboardMarkup{ InlineKeyboard: buttons }
|
||
}
|
||
|
||
// This is not a good solution
|
||
func deleteOldMessage(msgID int) {
|
||
if tsData.DeleteTimeoutInMinute == 0 { tsData.DeleteTimeoutInMinute = 10 }
|
||
time.Sleep(time.Minute * time.Duration(tsData.DeleteTimeoutInMinute))
|
||
_, err := privateOpts.Thebot.DeleteMessage(privateOpts.Ctx, &bot.DeleteMessageParams{
|
||
ChatID: tsData.GroupID,
|
||
MessageID: msgID,
|
||
})
|
||
if err != nil {
|
||
zerolog.Ctx(privateOpts.Ctx).Error().
|
||
Err(err).
|
||
Str("pluginName", "teamspeak3").
|
||
Str("funcName", "deleteOldMessage").
|
||
Int("messageID", msgID).
|
||
Int("deleteMessageMinute", tsData.DeleteTimeoutInMinute).
|
||
Str("content", "teamspeak user change notify").
|
||
Msg(flaterr.DeleteMessage.Str())
|
||
}
|
||
}
|