feat: add plugin Minecraft 服务器状态查询 (#1135)

This commit is contained in:
RefactoringHero
2025-03-25 11:30:40 +08:00
committed by GitHub
parent e6e6dd4565
commit e292b69ee5
11 changed files with 1414 additions and 89 deletions

View File

@@ -0,0 +1,300 @@
// Package minecraftobserver 通过mc服务器地址获取服务器状态信息并绘制图片发送到QQ群
package minecraftobserver
import (
"fmt"
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"
"strings"
"time"
)
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.Now().Sub(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
}