ZeroBot-Plugin/plugin/minecraftobserver/minecraftobserver.go
github-actions[bot] 6c6699a5d6
chore(lint): 改进代码样式 (#1143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-25 12:33:02 +09:00

302 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package minecraftobserver 通过mc服务器地址获取服务器状态信息并绘制图片发送到QQ群
package minecraftobserver
import (
"fmt"
"strings"
"time"
ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/control"
zbpCtxExt "github.com/FloatTech/zbputils/ctxext"
zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message"
)
const (
name = "minecraftobserver"
)
var (
// 注册插件
engine = control.AutoRegister(&ctrl.Options[*zero.Ctx]{
// 默认不启动
DisableOnDefault: false,
Brief: "Minecraft服务器状态查询/订阅",
// 详细帮助
Help: "- mc服务器状态 [服务器IP/URI]\n" +
"- mc服务器添加订阅 [服务器IP/URI]\n" +
"- mc服务器取消订阅 [服务器IP/URI]\n" +
"- mc服务器订阅拉取 (需要插件定时任务配合使用,全局只需要设置一个)" +
"-----------------------\n" +
"使用job插件设置定时, 例:" +
"记录在\"@every 1m\"触发的指令\n" +
"(机器人回答:您的下一条指令将被记录,在@@every 1m时触发" +
"mc服务器订阅拉取",
// 插件数据存储路径
PrivateDataFolder: name,
}).ApplySingle(zbpCtxExt.DefaultSingle)
)
func init() {
// 状态查询
engine.OnRegex("^[mM][cC]服务器状态 (.+)$").SetBlock(true).Handle(func(ctx *zero.Ctx) {
// 关键词查找
addr := ctx.State["regex_matched"].([]string)[1]
resp, err := getMinecraftServerStatus(addr)
if err != nil {
ctx.Send(message.Text("服务器状态获取失败... 错误信息: ", err))
return
}
status := resp.genServerSubscribeSchema(addr, 0)
textMsg, iconBase64 := status.generateServerStatusMsg()
var msg message.Message
if iconBase64 != "" {
msg = append(msg, message.Image(iconBase64))
}
msg = append(msg, message.Text(textMsg))
if id := ctx.Send(msg); id.ID() == 0 {
// logrus.Errorln(logPrefix + "Send failed")
return
}
})
// 添加订阅
engine.OnRegex(`^[mM][cC]服务器添加订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
// 关键词查找
addr := ctx.State["regex_matched"].([]string)[1]
status, err := getMinecraftServerStatus(addr)
if err != nil {
ctx.Send(message.Text("服务器信息初始化失败,请检查服务器是否可用!\n错误信息: ", err))
return
}
targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
err = dbInstance.newSubscribe(addr, targetID, targetType)
if err != nil {
ctx.Send(message.Text("订阅添加失败... 错误信息: ", err))
return
}
// 插入数据库(首条,需要更新状态)
err = dbInstance.updateServerStatus(status.genServerSubscribeSchema(addr, 0))
if err != nil {
ctx.Send(message.Text("服务器状态更新失败... 错误信息: ", err))
return
}
if sid := ctx.Send(message.Text(fmt.Sprintf("服务器 %s 订阅添加成功", addr))); sid.ID() == 0 {
// logrus.Errorln(logPrefix + "Send failed")
return
}
// 成功后立即发送一次状态
textMsg, iconBase64 := status.genServerSubscribeSchema(addr, 0).generateServerStatusMsg()
var msg message.Message
if iconBase64 != "" {
msg = append(msg, message.Image(iconBase64))
}
msg = append(msg, message.Text(textMsg))
if id := ctx.Send(msg); id.ID() == 0 {
// logrus.Errorln(logPrefix + "Send failed")
return
}
})
// 删除
engine.OnRegex(`^[mM][cC]服务器取消订阅\s*(.+)$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
addr := ctx.State["regex_matched"].([]string)[1]
// 通过群组id和服务器地址获取服务器状态
targetID, targetType := warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID)
err := dbInstance.deleteSubscribe(addr, targetID, targetType)
if err != nil {
ctx.Send(message.Text("取消订阅失败...", fmt.Sprintf("错误信息: %v", err)))
return
}
ctx.Send(message.Text("取消订阅成功"))
})
// 查看当前渠道的所有订阅
engine.OnRegex(`^[mM][cC]服务器订阅列表$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
subList, err := dbInstance.getSubscribesByTarget(warpTargetIDAndType(ctx.Event.GroupID, ctx.Event.UserID))
if err != nil {
ctx.Send(message.Text("获取订阅列表失败... 错误信息: ", err))
return
}
if len(subList) == 0 {
ctx.Send(message.Text("当前没有订阅哦"))
return
}
stringBuilder := strings.Builder{}
stringBuilder.WriteString("[订阅列表]\n")
for _, v := range subList {
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", v.ServerAddr))
}
if sid := ctx.Send(message.Text(stringBuilder.String())); sid.ID() == 0 {
// logrus.Errorln(logPrefix + "Send failed")
return
}
})
// 查看全局订阅情况(仅限管理员私聊可用)
engine.OnRegex(`^[mM][cC]服务器全局订阅列表$`, zero.OnlyPrivate, zero.SuperUserPermission, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
subList, err := dbInstance.getAllSubscribes()
if err != nil {
ctx.Send(message.Text("获取全局订阅列表失败... 错误信息: ", err))
return
}
if len(subList) == 0 {
ctx.Send(message.Text("当前一个订阅都没有哦"))
return
}
userID := ctx.Event.UserID
userName := ctx.CardOrNickName(userID)
msg := make(message.Message, 0)
// 按照群组or用户分组来定
groupSubMap := make(map[int64][]serverSubscribe)
userSubMap := make(map[int64][]serverSubscribe)
for _, v := range subList {
switch v.TargetType {
case targetTypeGroup:
groupSubMap[v.TargetID] = append(groupSubMap[v.TargetID], v)
case targetTypeUser:
userSubMap[v.TargetID] = append(userSubMap[v.TargetID], v)
default:
}
}
// 群
for k, v := range groupSubMap {
stringBuilder := strings.Builder{}
stringBuilder.WriteString(fmt.Sprintf("[群 %d]存在以下订阅:\n", k))
for _, sub := range v {
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
}
msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
}
// 个人
for k, v := range userSubMap {
stringBuilder := strings.Builder{}
stringBuilder.WriteString(fmt.Sprintf("[用户 %d]存在以下订阅:\n", k))
for _, sub := range v {
stringBuilder.WriteString(fmt.Sprintf("服务器地址: %s\n", sub.ServerAddr))
}
msg = append(msg, message.CustomNode(userName, userID, stringBuilder.String()))
}
// 合并发送
ctx.SendPrivateForwardMessage(ctx.Event.UserID, msg)
})
// 状态变更通知,全局触发,逐个服务器检查,检查到变更则逐个发送通知
engine.OnRegex(`^[mM][cC]服务器订阅拉取$`, getDB).SetBlock(true).Handle(func(ctx *zero.Ctx) {
serverList, err := dbInstance.getAllSubscribes()
if err != nil {
su := zero.BotConfig.SuperUsers[0]
// 如果订阅列表获取失败,通知管理员
ctx.SendPrivateMessage(su, message.Text(logPrefix, "获取订阅列表失败..."))
return
}
// logrus.Debugln(logPrefix+"global get ", len(serverList), " subscribe(s)")
serverMap := make(map[string][]serverSubscribe)
for _, v := range serverList {
serverMap[v.ServerAddr] = append(serverMap[v.ServerAddr], v)
}
changedCount := 0
for subAddr, oneServerSubList := range serverMap {
// 查询当前存储的状态
storedStatus, sErr := dbInstance.getServerStatus(subAddr)
if sErr != nil {
// logrus.Errorln(logPrefix+fmt.Sprintf("getServerStatus ServerAddr(%s) error: ", subAddr), sErr)
continue
}
isChanged, changedNotifyMsg, sErr := singleServerScan(storedStatus)
if sErr != nil {
// logrus.Errorln(logPrefix+"singleServerScan error: ", sErr)
continue
}
if !isChanged {
continue
}
changedCount++
// 发送变化信息
for _, subInfo := range oneServerSubList {
time.Sleep(100 * time.Millisecond)
if subInfo.TargetType == targetTypeUser {
ctx.SendPrivateMessage(subInfo.TargetID, changedNotifyMsg)
} else if subInfo.TargetType == targetTypeGroup {
m, ok := control.Lookup(name)
if !ok {
continue
}
if !m.IsEnabledIn(subInfo.TargetID) {
continue
}
ctx.SendGroupMessage(subInfo.TargetID, changedNotifyMsg)
}
}
}
})
}
// singleServerScan 单个服务器状态扫描
func singleServerScan(oldSubStatus *serverStatus) (changed bool, notifyMsg message.Message, err error) {
notifyMsg = make(message.Message, 0)
newSubStatus := &serverStatus{}
// 获取服务器状态 & 检查是否需要更新
rawServerStatus, err := getMinecraftServerStatus(oldSubStatus.ServerAddr)
if err != nil {
// logrus.Warnln(logPrefix+"getMinecraftServerStatus error: ", err)
err = nil
// 计数器没有超限,增加计数器并跳过
if cnt, ts := addPingServerUnreachableCounter(oldSubStatus.ServerAddr, time.Now()); cnt < pingServerUnreachableCounterThreshold &&
time.Since(ts) < pingServerUnreachableCounterTimeThreshold {
// logrus.Warnln(logPrefix+"server ", oldSubStatus.ServerAddr, " unreachable, counter: ", cnt, " ts:", ts)
return
}
// 不可达计数器已经超限,则更新服务器状态
// 深拷贝设置PingDelay为不可达
newSubStatus = oldSubStatus.deepCopy()
newSubStatus.PingDelay = pingDelayUnreachable
} else {
newSubStatus = rawServerStatus.genServerSubscribeSchema(oldSubStatus.ServerAddr, oldSubStatus.ID)
}
if newSubStatus == nil {
// logrus.Errorln(logPrefix + "newSubStatus is nil")
return
}
// 检查是否有订阅信息变化
if oldSubStatus.isServerStatusSpecChanged(newSubStatus) {
// logrus.Warnf(logPrefix+"server subscribe spec changed: (%+v) -> (%+v)", oldSubStatus, newSubStatus)
changed = true
// 更新数据库
err = dbInstance.updateServerStatus(newSubStatus)
if err != nil {
// logrus.Errorln(logPrefix+"updateServerSubscribeStatus error: ", err)
return
}
// 纯文本信息
notifyMsg = append(notifyMsg, message.Text(formatSubStatusChangeText(oldSubStatus, newSubStatus)))
// 如果有图标变更
if oldSubStatus.FaviconMD5 != newSubStatus.FaviconMD5 {
// 有图标变更
notifyMsg = append(notifyMsg, message.Text("\n-----[图标变更]-----\n"))
// 旧图标
notifyMsg = append(notifyMsg, message.Text("[旧]\n"))
if oldSubStatus.FaviconRaw != "" {
notifyMsg = append(notifyMsg, message.Image(oldSubStatus.FaviconRaw.toBase64String()))
} else {
notifyMsg = append(notifyMsg, message.Text("(空)\n"))
}
// 新图标
notifyMsg = append(notifyMsg, message.Text("[新]\n"))
if newSubStatus.FaviconRaw != "" {
notifyMsg = append(notifyMsg, message.Image(newSubStatus.FaviconRaw.toBase64String()))
} else {
notifyMsg = append(notifyMsg, message.Text("(空)\n"))
}
}
notifyMsg = append(notifyMsg, message.Text("\n-------最新状态-------\n"))
// 服务状态
textMsg, iconBase64 := newSubStatus.generateServerStatusMsg()
if iconBase64 != "" {
notifyMsg = append(notifyMsg, message.Image(iconBase64))
}
notifyMsg = append(notifyMsg, message.Text(textMsg))
}
// 逻辑到达这里,说明状态已经变更 or 无变更且服务器可达,重置不可达计数器
resetPingServerUnreachableCounter(oldSubStatus.ServerAddr)
return
}