Files
cellbot/internal/plugins/mcstatus/mcstatus.go
lafay fb5fae1524 chore: update project structure and enhance plugin functionality
- Added new entries to .gitignore for database files.
- Updated go.mod and go.sum to include new indirect dependencies for database and ORM support.
- Refactored event handling to improve message reply functionality in the protocol.
- Enhanced the dispatcher to allow for better event processing and logging.
- Removed outdated plugin documentation and unnecessary files to streamline the codebase.
- Improved welcome message formatting and screenshot options for better user experience.
2026-01-05 05:14:31 +08:00

376 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 进行截图,避免受 dispatcher context 影响
// 如果 dispatcher context 被取消,截图操作仍能完成
screenshotCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// 渲染并截图
chain, err := utils.ScreenshotHTMLToMessageChain(screenshotCtx, htmlTemplate, opts)
if err != 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)
}