mirror of
https://github.com/awfufu/go-hurobot.git
synced 2026-03-01 05:29:43 +08:00
feat: add RCON integration and Minecraft command system
- Add RCON configuration management with `rcon` command - Add Minecraft command execution with `mc` command - Add automatic message forwarding from groups to Minecraft - Add group_rcon_configs database table for RCON settings
This commit is contained in:
@@ -42,6 +42,8 @@ func init() {
|
||||
"dice": cmd_dice,
|
||||
"memberinfo": cmd_memberinfo,
|
||||
"info": cmd_info,
|
||||
"rcon": cmd_rcon,
|
||||
"mc": cmd_mc,
|
||||
}
|
||||
|
||||
for key := range cmdMap {
|
||||
|
||||
223
cmds/mc.go
Normal file
223
cmds/mc.go
Normal file
@@ -0,0 +1,223 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go-hurobot/config"
|
||||
"go-hurobot/qbot"
|
||||
|
||||
"github.com/gorcon/rcon"
|
||||
)
|
||||
|
||||
func cmd_mc(c *qbot.Client, raw *qbot.Message, args *ArgsList) {
|
||||
if args.Size < 2 {
|
||||
c.SendMsg(raw, "Usage: mc <command>")
|
||||
return
|
||||
}
|
||||
|
||||
// Get RCON configuration for this group
|
||||
var rconConfig qbot.GroupRconConfigs
|
||||
result := qbot.PsqlDB.Where("group_id = ?", raw.GroupID).First(&rconConfig)
|
||||
|
||||
if result.Error != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !rconConfig.Enabled {
|
||||
c.SendMsg(raw, "RCON is disabled for this group")
|
||||
return
|
||||
}
|
||||
|
||||
// Join all arguments after 'mc' as the command
|
||||
command := strings.Join(args.Contents[1:], " ")
|
||||
|
||||
// Check permissions for non-master users
|
||||
if raw.UserID != config.MasterID && !isAllowedCommand(command) {
|
||||
c.SendMsg(raw, "Permission denied. You can only use query commands.")
|
||||
return
|
||||
}
|
||||
|
||||
// Execute RCON command
|
||||
response, err := executeRconCommand(rconConfig.Address, rconConfig.Password, command)
|
||||
if err != nil {
|
||||
c.SendMsg(raw, fmt.Sprintf("RCON error: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Send response back (limit to avoid spam)
|
||||
if len(response) > 1000 {
|
||||
response = response[:1000] + "... (truncated)"
|
||||
}
|
||||
|
||||
if response == "" {
|
||||
response = "No output"
|
||||
}
|
||||
|
||||
c.SendMsg(raw, qbot.CQReply(raw.UserID)+response)
|
||||
}
|
||||
|
||||
func executeRconCommand(address, password, command string) (string, error) {
|
||||
// Connect to RCON server
|
||||
conn, err := rcon.Dial(address, password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Execute command
|
||||
response, err := conn.Execute(command)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed: %w", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ForwardMessageToMC forwards a group message to Minecraft server if RCON is enabled
|
||||
func ForwardMessageToMC(c *qbot.Client, msg *qbot.Message) {
|
||||
// Skip bot's own messages
|
||||
if msg.UserID == config.BotID {
|
||||
return
|
||||
}
|
||||
|
||||
// Get RCON configuration for this group
|
||||
var rconConfig qbot.GroupRconConfigs
|
||||
result := qbot.PsqlDB.Where("group_id = ?", msg.GroupID).First(&rconConfig)
|
||||
|
||||
// Skip if RCON not configured or disabled
|
||||
if result.Error != nil || !rconConfig.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// Get user's nickname from database
|
||||
var user qbot.Users
|
||||
nickname := msg.Card // Default to group card name
|
||||
|
||||
userResult := qbot.PsqlDB.Where("user_id = ?", msg.UserID).First(&user)
|
||||
if userResult.Error == nil && user.Nickname != "" {
|
||||
nickname = user.Nickname
|
||||
}
|
||||
|
||||
// Clean the message content for Minecraft (remove special characters)
|
||||
cleanContent := cleanMessageForMC(msg.Content)
|
||||
|
||||
// Create tellraw command
|
||||
tellrawCmd := fmt.Sprintf("tellraw @a {\"text\":\"<%s> %s\"}",
|
||||
escapeMinecraftText(nickname),
|
||||
escapeMinecraftText(cleanContent))
|
||||
|
||||
// Execute the command
|
||||
executeRconCommand(rconConfig.Address, rconConfig.Password, tellrawCmd)
|
||||
}
|
||||
|
||||
// cleanMessageForMC removes or replaces characters that might cause issues in Minecraft
|
||||
func cleanMessageForMC(content string) string {
|
||||
// Remove or replace problematic characters
|
||||
content = strings.ReplaceAll(content, "\n", " ")
|
||||
content = strings.ReplaceAll(content, "\r", " ")
|
||||
content = strings.ReplaceAll(content, "\t", " ")
|
||||
|
||||
// Remove multiple spaces
|
||||
for strings.Contains(content, " ") {
|
||||
content = strings.ReplaceAll(content, " ", " ")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
// escapeMinecraftText escapes special characters for Minecraft JSON text
|
||||
func escapeMinecraftText(text string) string {
|
||||
text = strings.ReplaceAll(text, "\\", "\\\\")
|
||||
text = strings.ReplaceAll(text, "\"", "\\\"")
|
||||
text = strings.ReplaceAll(text, "\n", "\\n")
|
||||
text = strings.ReplaceAll(text, "\r", "\\r")
|
||||
text = strings.ReplaceAll(text, "\t", "\\t")
|
||||
return text
|
||||
}
|
||||
|
||||
// isAllowedCommand checks if a command is allowed for non-master users
|
||||
func isAllowedCommand(command string) bool {
|
||||
// Remove leading slash if present
|
||||
command = strings.TrimPrefix(command, "/")
|
||||
|
||||
// Split command into parts for analysis
|
||||
parts := strings.Fields(strings.ToLower(command))
|
||||
if len(parts) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
mainCmd := parts[0]
|
||||
|
||||
// Allowed commands for non-master users (query/read-only commands)
|
||||
switch mainCmd {
|
||||
case "list":
|
||||
return true
|
||||
case "seed":
|
||||
return true
|
||||
case "version":
|
||||
return true
|
||||
case "data":
|
||||
// Only allow "data get" commands
|
||||
if len(parts) >= 2 && parts[1] == "get" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case "team":
|
||||
// Only allow "team list"
|
||||
if len(parts) >= 2 && parts[1] == "list" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case "whitelist":
|
||||
// Only allow "whitelist list"
|
||||
if len(parts) >= 2 && parts[1] == "list" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case "banlist":
|
||||
return true
|
||||
case "locate":
|
||||
// Allow all locate subcommands (structure, biome, poi)
|
||||
return true
|
||||
case "worldborder":
|
||||
// Only allow "worldborder get"
|
||||
if len(parts) >= 2 && parts[1] == "get" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case "datapack":
|
||||
// Only allow "datapack list"
|
||||
if len(parts) >= 2 && parts[1] == "list" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case "function":
|
||||
// Allow function queries (without execution)
|
||||
// This is tricky - for safety, only allow when no arguments that suggest execution
|
||||
if len(parts) == 1 {
|
||||
return true // Just "function" command shows help
|
||||
}
|
||||
return false
|
||||
case "gamerule":
|
||||
// Allow gamerule queries (when no value is being set)
|
||||
if len(parts) <= 2 {
|
||||
return true // "gamerule" or "gamerule <rule>" (query)
|
||||
}
|
||||
return false // "gamerule <rule> <value>" (modification)
|
||||
case "difficulty":
|
||||
// Allow difficulty query (when no value is being set)
|
||||
if len(parts) == 1 {
|
||||
return true // Just "difficulty" (query)
|
||||
}
|
||||
return false // "difficulty <value>" (modification)
|
||||
case "defaultgamemode":
|
||||
// Allow defaultgamemode query (when no value is being set)
|
||||
if len(parts) == 1 {
|
||||
return true // Just "defaultgamemode" (query)
|
||||
}
|
||||
return false // "defaultgamemode <value>" (modification)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
135
cmds/rcon.go
Normal file
135
cmds/rcon.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package cmds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go-hurobot/config"
|
||||
"go-hurobot/qbot"
|
||||
)
|
||||
|
||||
func cmd_rcon(c *qbot.Client, raw *qbot.Message, args *ArgsList) {
|
||||
if raw.UserID != config.MasterID {
|
||||
return
|
||||
}
|
||||
|
||||
const help = `Usage: rcon [status | set <address> <password> | enable | disable]
|
||||
|
||||
Examples:
|
||||
rcon status
|
||||
rcon set '127.0.0.1:25575' 'password'
|
||||
rcon enable
|
||||
rcon disable`
|
||||
|
||||
if args.Size == 1 {
|
||||
c.SendMsg(raw, help)
|
||||
return
|
||||
}
|
||||
|
||||
switch args.Contents[1] {
|
||||
case "status":
|
||||
showRconStatus(c, raw)
|
||||
case "set":
|
||||
if args.Size != 4 {
|
||||
c.SendMsg(raw, "Usage: rcon set <address> <password>")
|
||||
return
|
||||
}
|
||||
setRconConfig(c, raw, args.Contents[2], args.Contents[3])
|
||||
case "enable":
|
||||
toggleRcon(c, raw, true)
|
||||
case "disable":
|
||||
toggleRcon(c, raw, false)
|
||||
default:
|
||||
c.SendMsg(raw, help)
|
||||
}
|
||||
}
|
||||
|
||||
func showRconStatus(c *qbot.Client, msg *qbot.Message) {
|
||||
var config qbot.GroupRconConfigs
|
||||
result := qbot.PsqlDB.Where("group_id = ?", msg.GroupID).First(&config)
|
||||
|
||||
if result.Error != nil {
|
||||
c.SendMsg(msg, "RCON not configured for this group")
|
||||
return
|
||||
}
|
||||
|
||||
status := "disabled"
|
||||
if config.Enabled {
|
||||
status = "enabled"
|
||||
}
|
||||
|
||||
// Hide password for security
|
||||
maskedPassword := strings.Repeat("*", len(config.Password))
|
||||
response := fmt.Sprintf("RCON Status: %s\nAddress: %s\nPassword: %s",
|
||||
status, config.Address, maskedPassword)
|
||||
|
||||
c.SendMsg(msg, response)
|
||||
}
|
||||
|
||||
func setRconConfig(c *qbot.Client, msg *qbot.Message, address, password string) {
|
||||
// Validate address format (should contain port)
|
||||
if !strings.Contains(address, ":") {
|
||||
c.SendMsg(msg, "Invalid address format. Use host:port (e.g., 127.0.0.1:25575)")
|
||||
return
|
||||
}
|
||||
|
||||
// Validate port
|
||||
parts := strings.Split(address, ":")
|
||||
if len(parts) != 2 {
|
||||
c.SendMsg(msg, "Invalid address format. Use host:port")
|
||||
return
|
||||
}
|
||||
|
||||
if port, err := strconv.Atoi(parts[1]); err != nil || port < 1 || port > 65535 {
|
||||
c.SendMsg(msg, "Invalid port number")
|
||||
return
|
||||
}
|
||||
|
||||
config := qbot.GroupRconConfigs{
|
||||
GroupID: msg.GroupID,
|
||||
Address: address,
|
||||
Password: password,
|
||||
Enabled: false, // Default to disabled for security
|
||||
}
|
||||
|
||||
// Use Upsert to create or update
|
||||
result := qbot.PsqlDB.Where("group_id = ?", msg.GroupID).Assign(
|
||||
qbot.GroupRconConfigs{
|
||||
Address: address,
|
||||
Password: password,
|
||||
},
|
||||
).FirstOrCreate(&config)
|
||||
|
||||
if result.Error != nil {
|
||||
c.SendMsg(msg, "Database error: "+result.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.SendMsg(msg, fmt.Sprintf("RCON configuration updated:\nAddress: %s\nStatus: disabled (use 'rcon enable' to enable)", address))
|
||||
}
|
||||
|
||||
func toggleRcon(c *qbot.Client, msg *qbot.Message, enabled bool) {
|
||||
// Check if configuration exists
|
||||
var config qbot.GroupRconConfigs
|
||||
result := qbot.PsqlDB.Where("group_id = ?", msg.GroupID).First(&config)
|
||||
|
||||
if result.Error != nil {
|
||||
c.SendMsg(msg, "RCON not configured for this group. Use 'rcon set' first.")
|
||||
return
|
||||
}
|
||||
|
||||
// Update enabled status
|
||||
result = qbot.PsqlDB.Model(&config).Where("group_id = ?", msg.GroupID).Update("enabled", enabled)
|
||||
|
||||
if result.Error != nil {
|
||||
c.SendMsg(msg, "Database error: "+result.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
status := "disabled"
|
||||
if enabled {
|
||||
status = "enabled"
|
||||
}
|
||||
c.SendMsg(msg, fmt.Sprintf("RCON %s for this group", status))
|
||||
}
|
||||
@@ -46,3 +46,11 @@ CREATE TABLE user_events (
|
||||
CONSTRAINT check_rand_prob CHECK (rand_prob >= 0.0 AND rand_prob <= 1.0),
|
||||
CONSTRAINT check_event_idx CHECK (event_idx >= 0 AND event_idx <= 9)
|
||||
);
|
||||
|
||||
CREATE TABLE group_rcon_configs (
|
||||
"group_id" BIGINT NOT NULL,
|
||||
"address" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("group_id")
|
||||
);
|
||||
|
||||
1
go.mod
1
go.mod
@@ -26,6 +26,7 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/gorcon/rcon v1.4.0 // indirect
|
||||
github.com/jinzhu/copier v0.3.5 // indirect
|
||||
github.com/joho/godotenv v1.4.0 // indirect
|
||||
github.com/longportapp/openapi-protobufs/gen/go v0.5.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -35,6 +35,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorcon/rcon v1.4.0 h1:pYwZ8Rhcgfh/LhdPBncecuEo5thoFvPIuMSWovz1FME=
|
||||
github.com/gorcon/rcon v1.4.0/go.mod h1:M6v6sNmr/NET9YIf+2rq+cIjTBridoy62uzQ58WgC1I=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
|
||||
@@ -11,6 +11,12 @@ func messageHandler(c *qbot.Client, msg *qbot.Message) {
|
||||
if msg.UserID != config.BotID {
|
||||
isCommand := cmds.HandleCommand(c, msg)
|
||||
defer qbot.SaveDatabase(msg, isCommand)
|
||||
|
||||
// Forward non-command messages to Minecraft if RCON is enabled
|
||||
if !isCommand {
|
||||
cmds.ForwardMessageToMC(c, msg)
|
||||
}
|
||||
|
||||
if isCommand {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,13 +39,20 @@ type UserEvents struct {
|
||||
CreatedAt time.Time `gorm:"not null;column:created_at;default:now()"`
|
||||
}
|
||||
|
||||
type GroupRconConfigs struct {
|
||||
GroupID uint64 `gorm:"primaryKey;column:group_id"`
|
||||
Address string `gorm:"not null;column:address"`
|
||||
Password string `gorm:"not null;column:password"`
|
||||
Enabled bool `gorm:"not null;column:enabled;default:false"`
|
||||
}
|
||||
|
||||
func initPsqlDB(dsn string) error {
|
||||
var err error
|
||||
if PsqlDB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}); err != nil {
|
||||
return err
|
||||
}
|
||||
PsqlConnected = true
|
||||
return PsqlDB.AutoMigrate(&Users{}, &Messages{}, &UserEvents{})
|
||||
return PsqlDB.AutoMigrate(&Users{}, &Messages{}, &UserEvents{}, &GroupRconConfigs{})
|
||||
}
|
||||
|
||||
func SaveDatabase(msg *Message, isCmd bool) error {
|
||||
|
||||
Reference in New Issue
Block a user