Files
cellbot/internal/plugins/mcstatus/mcstatus.go
xiaolan f3a72264af chore: update dependencies and refactor webhook handling
- Added new dependencies for SQLite support and improved HTTP client functionality in go.mod and go.sum.
- Refactored webhook server implementation to utilize a simplified version, enhancing code maintainability.
- Updated API client to leverage a generic request method, streamlining API interactions.
- Modified configuration to include access token for webhook server, improving security.
- Enhanced event handling and request processing in the API client for better performance.
2026-01-05 18:42:45 +08:00

381 lines
10 KiB
Go
Raw 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 mcstatus
import (
"context"
"fmt"
"strings"
"time"
"cellbot/internal/database"
"cellbot/internal/engine"
"cellbot/internal/protocol"
"cellbot/pkg/utils"
"go.uber.org/zap"
)
// ServerBind 服务器绑定模型
type ServerBind struct {
ID string `gorm:"primaryKey"` // group_id
ServerIP string `gorm:"not null"` // 服务器IP
}
// TableName 指定表名(基础表名,实际使用时需要添加 botID 前缀)
func (ServerBind) TableName() string {
return "mc_server_binds"
}
var dbService database.Database
func init() {
// 注册命令(数据库将在依赖注入时初始化)
engine.OnCommand("/mcs").
Priority(100).
Handle(handleMCSCommand)
engine.OnCommand("/mcsBind").
Priority(100).
Handle(handleMCSBindCommand)
}
// InitDatabase 初始化数据库(由依赖注入调用)
func InitDatabase(database database.Database) error {
dbService = database
return nil
}
// handleMCSCommand 处理 /mcs 命令
func handleMCSCommand(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
logger.Info("handleMCSCommand called",
zap.String("self_id", event.GetSelfID()),
zap.String("detail_type", event.GetDetailType()))
data := event.GetData()
// 获取原始消息内容
rawMessage, ok := data["raw_message"].(string)
if !ok {
logger.Warn("raw_message not found in event data")
return nil
}
logger.Info("Processing /mcs command",
zap.String("raw_message", rawMessage))
// 解析命令参数(/mcs 后面的内容)
var serverIP string
parts := strings.Fields(rawMessage)
if len(parts) > 1 {
serverIP = strings.Join(parts[1:], " ")
}
// 如果没有提供 IP尝试从群绑定或默认配置获取
if serverIP == "" {
groupID, _ := data["group_id"]
botID := event.GetSelfID()
if groupID != nil && dbService != nil && botID != "" {
db, err := dbService.GetDB(botID)
if err != nil {
logger.Warn("Failed to get database for bot",
zap.String("bot_id", botID),
zap.Error(err))
} else {
var bind ServerBind
tableName := database.GetTableName(botID, "mc_server_binds")
if err := db.Table(tableName).Where("id = ?", fmt.Sprintf("%v", groupID)).First(&bind).Error; err != nil {
logger.Debug("No server bind found for group",
zap.String("bot_id", botID),
zap.Any("group_id", groupID),
zap.Error(err))
} else {
serverIP = bind.ServerIP
}
}
}
// 如果还是没有,使用默认值
if serverIP == "" {
serverIP = "mc.hypixel.net" // 默认服务器
}
}
// 解析服务器地址和端口
host := serverIP
port := 25565
if strings.Contains(serverIP, ":") {
parts := strings.Split(serverIP, ":")
host = parts[0]
fmt.Sscanf(parts[1], "%d", &port)
}
logger.Info("Querying Minecraft server",
zap.String("host", host),
zap.Int("port", port))
// 查询服务器状态
status, err := Ping(host, port, 10*time.Second)
if err != nil {
logger.Error("Failed to ping server", zap.Error(err))
// 使用 ReplyText 发送错误消息
errorMsg := fmt.Sprintf("查询失败: %v", err)
event.ReplyText(ctx, botManager, logger, errorMsg)
return err
}
// 构建 HTML 模板
htmlTemplate := buildStatusHTML(status)
// 配置截图选项
opts := &utils.ScreenshotOptions{
Width: 1200,
Height: 800,
Timeout: 60 * time.Second,
WaitTime: 3 * time.Second,
FullPage: false,
Quality: 90,
Logger: logger,
}
// 使用独立的 context 进行截图,完全避免受外部 context 影响
// 使用更长的超时时间,避免频繁失败
screenshotCtx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
// 渲染并截图
chain, err := utils.ScreenshotHTMLToMessageChain(screenshotCtx, htmlTemplate, opts)
if err != nil {
// context.Canceled 是应用关闭时的正常行为,不记录为错误
if err.Error() == "screenshot operation was canceled" {
logger.Warn("Screenshot canceled due to application shutdown")
return nil
}
logger.Error("Failed to render status image", zap.Error(err))
return err
}
// 使用 Reply 发送图片
err = event.Reply(ctx, botManager, logger, chain)
if err != nil {
logger.Error("Failed to send status image", zap.Error(err))
return err
}
logger.Info("Minecraft server status sent",
zap.String("host", host),
zap.Int("port", port))
return nil
}
// handleMCSBindCommand 处理 /mcsBind 命令
func handleMCSBindCommand(ctx context.Context, event protocol.Event, botManager *protocol.BotManager, logger *zap.Logger) error {
data := event.GetData()
// 只允许群聊使用
if event.GetDetailType() != "group" {
return event.ReplyText(ctx, botManager, logger, "此命令只能在群聊中使用")
}
// 获取原始消息内容
rawMessage, ok := data["raw_message"].(string)
if !ok {
return nil
}
// 解析命令参数(/mcsBind 后面的内容)
parts := strings.Fields(rawMessage)
if len(parts) < 2 {
groupID, _ := data["group_id"]
errorMsg := protocol.NewMessageChain(
protocol.NewTextSegment("用法: /mcsBind <服务器IP>"),
)
action := &protocol.BaseAction{
Type: protocol.ActionTypeSendGroupMessage,
Params: map[string]interface{}{
"group_id": groupID,
"message": errorMsg,
},
}
selfID := event.GetSelfID()
bot, ok := botManager.Get(selfID)
if !ok {
bots := botManager.GetAll()
if len(bots) > 0 {
bot = bots[0]
}
}
if bot != nil {
bot.SendAction(ctx, action)
}
return nil
}
serverIP := strings.Join(parts[1:], " ")
groupID, _ := data["group_id"]
groupIDStr := fmt.Sprintf("%v", groupID)
botID := event.GetSelfID()
// 保存绑定到数据库
if dbService != nil && botID != "" {
db, err := dbService.GetDB(botID)
if err == nil {
// 自动迁移表(如果不存在)
tableName := database.GetTableName(botID, "mc_server_binds")
if err := db.Table(tableName).AutoMigrate(&ServerBind{}); err != nil {
logger.Error("Failed to migrate table", zap.Error(err))
} else {
bind := ServerBind{
ID: groupIDStr,
ServerIP: serverIP,
}
// 使用 Save 方法,如果存在则更新,不存在则创建
if err := db.Table(tableName).Save(&bind).Error; err != nil {
logger.Error("Failed to save server bind", zap.Error(err))
}
}
}
}
// 使用 ReplyText 发送确认消息
successMsg := fmt.Sprintf("已绑定服务器 %s 到本群", serverIP)
err := event.ReplyText(ctx, botManager, logger, successMsg)
if err != nil {
return err
}
logger.Info("Minecraft server bound",
zap.String("group_id", groupIDStr),
zap.String("server_ip", serverIP))
return nil
}
// buildStatusHTML 构建服务器状态 HTML
func buildStatusHTML(status *ServerStatus) string {
iconHTML := ""
if status.Favicon != "" {
iconHTML = fmt.Sprintf(`<img src="data:image/png;base64,%s" alt="icon" style="width: 64px; height: 64px; border-radius: 8px;" />`, status.Favicon)
}
return fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #2e3440 0%%, #434c5e 100%%);
padding: 40px 20px;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
color: #cdd6f4;
}
.container {
background: #2e3440;
border-radius: 20px;
padding: 40px;
max-width: 800px;
width: 100%%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.header {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
}
.icon {
flex-shrink: 0;
}
.title {
font-size: 28px;
font-weight: bold;
color: #cdd6f4;
}
.info-section {
background: #434c5e;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.info-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #3b4252;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #81a1c1;
}
.info-value {
color: #cdd6f4;
}
.description {
background: #3b4252;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
color: #cdd6f4;
font-size: 16px;
}
.footer {
text-align: center;
margin-top: 30px;
color: #81a1c1;
font-size: 14px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
%s
<div class="title">Minecraft 服务器状态</div>
</div>
<div class="info-section">
<div class="info-item">
<span class="info-label">服务器地址:</span>
<span class="info-value">%s</span>
</div>
<div class="info-item">
<span class="info-label">延迟:</span>
<span class="info-value">%d ms</span>
</div>
<div class="info-item">
<span class="info-label">版本:</span>
<span class="info-value">%s (协议 %d)</span>
</div>
<div class="info-item">
<span class="info-label">在线人数:</span>
<span class="info-value">%d / %d</span>
</div>
</div>
<div class="description">
<strong>服务器描述:</strong><br>
%s
</div>
<div class="footer">
Generated by mc-server-status
</div>
</div>
</body>
</html>
`, iconHTML, fmt.Sprintf("%s:%d", status.Host, status.Port), status.Latency, status.Version.Name, status.Version.Protocol, status.Players.Online, status.Players.Max, status.Description)
}