diff --git a/config/variables.go b/config/variables.go index 8bdd2f6..9abae52 100644 --- a/config/variables.go +++ b/config/variables.go @@ -12,6 +12,8 @@ type DebugConfig struct { ShowNapcatMsg bool `json:"showNapcatMsg"` ShowNapcatAction bool `json:"showNapcatAction"` ShowTelegramMsg bool `json:"showTelegramMsg"` + ShowTriggerCmdEcho bool `json:"showTriggerCmdEcho"` + NapcatIgnoreSelfMsg bool `json:"napcatIgnoreSelfMsg"` } // KomariAccount holds Komari login credentials. diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 9bbf549..94d3d84 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -31,12 +31,6 @@ type Controller interface { SendExecuteResult(serverName, serverUUID, command, result string) error } -// CommandController extends Controller for bidirectional channels (QQ, Telegram). -type CommandController interface { - Controller - HandleCommand(cmd Command) (response string, err error) -} - // Manager manages all controller instances and routes events. type Manager struct { mu sync.RWMutex diff --git a/internal/controller/pipes/email.go b/internal/controller/pipes/email.go index 785db16..870ac27 100644 --- a/internal/controller/pipes/email.go +++ b/internal/controller/pipes/email.go @@ -46,9 +46,6 @@ func (e *EmailController) IsEnabled() bool { return e.cfg.Enabled } -// HandleCommand is not supported for Email (status-only controller). -// This controller does not implement CommandController. - // SendStatusChange sends a status change notification via Email. func (e *EmailController) SendStatusChange(change node.StatusChange) error { if !e.cfg.Enabled { diff --git a/internal/controller/pipes/qq_napcat/qq.go b/internal/controller/pipes/qq_napcat/qq.go index f4ea6ae..ea85453 100644 --- a/internal/controller/pipes/qq_napcat/qq.go +++ b/internal/controller/pipes/qq_napcat/qq.go @@ -7,7 +7,6 @@ import ( "nukumizu-backend/config" "nukumizu-backend/internal/controller" - "nukumizu-backend/internal/komari" "nukumizu-backend/internal/node" "nukumizu-backend/internal/template" "nukumizu-backend/postLog" @@ -16,18 +15,18 @@ import ( // oneBotEvent mirrors a OneBot 11 event pushed over the NapCat WebSocket. // Only "message" events are handled; notice/request/meta_event are ignored. type oneBotEvent struct { - PostType string `json:"post_type"` - MessageType string `json:"message_type"` - GroupID int64 `json:"group_id"` - UserID int64 `json:"user_id"` - RawMessage string `json:"raw_message"` - Message json.RawMessage `json:"message"` - Sender struct { - UserID int64 `json:"user_id"` - Nickname string `json:"nickname"` + PostType string `json:"post_type"` // Identifies "message", "notice", "request", or "meta_event" + MessageType string `json:"message_type"` // Identifies "private" or "group" + GroupID int64 `json:"group_id"` // Only present for group messages + UserID int64 `json:"user_id"` // The sender's QQ ID + RawMessage string `json:"raw_message"` // The raw message text + Message json.RawMessage `json:"message"` // The message content, which may include CQ codes + Sender struct { // The sender's information + UserID int64 `json:"user_id"` // The sender's QQ ID + Nickname string `json:"nickname"` // The sender's nickname } `json:"sender"` - SelfID int64 `json:"self_id"` - SubType string `json:"sub_type"` + SelfID int64 `json:"self_id"` // The bot's QQ ID + SubType string `json:"sub_type"` // The subtype of the message, e.g., "normal", "anonymous", etc. } // QQController handles QQ Bot interactions by connecting directly to NapCat. @@ -101,11 +100,20 @@ func (q *QQController) handleNapcatEvent(raw []byte) { // Only handle message events; ignore notice/request/meta_event. if ev.PostType != "message" { + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("Ignoring Napcat WS event: " + string(raw)) + } return } // Ignore messages the bot itself sent (echo prevention). - if q.isSelfMessage(ev) { + if q.isSelfMessage(ev) && !config.GetConfig().System.DebugMode { + return + } + if q.isSelfMessage(ev) && config.GetConfig().System.DebugMode && config.GetConfig().Debug.NapcatIgnoreSelfMsg { + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("Ignoring Napcat WS self message: " + string(raw)) + } return } @@ -115,7 +123,6 @@ func (q *QQController) handleNapcatEvent(raw []byte) { chatID = ev.UserID } - // Build a command from the raw message. cmd := controller.Command{ RawText: ev.RawMessage, ChatID: chatID, @@ -123,13 +130,11 @@ func (q *QQController) handleNapcatEvent(raw []byte) { SenderID: ev.UserID, } - response, err := q.HandleCommand(cmd) - if err != nil { - postLog.Error("Napcat command handling failed: " + err.Error()) - return - } - + response := q.processCommand(cmd) if response == "" { + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("Napcat WS command discarded: " + string(raw)) + } return } @@ -145,6 +150,54 @@ func (q *QQController) handleNapcatEvent(raw []byte) { } } +// processCommand validates an incoming message as a command and hands the +// complete command to the unified processor. It returns the response text to +// reply with; an empty response means the message was discarded. +func (q *QQController) processCommand(cmd controller.Command) string { + cfg := config.GetConfig() + + // Debug: when ShowNapcatMsg is enabled, echo the received message back in real-time. + if cfg.Debug.ShowNapcatMsg { + postLog.Debug("Napcat message received: " + cmd.RawText) + return cmd.RawText + } + + text := cmd.RawText + + // In "at" listen mode, require an @mention of the bot and strip it before + // parsing, otherwise raw_message like "[CQ:at,qq=123] /list" fails the + // "/" prefix check. + if q.cfg.ListenMethod == "at" { + atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID) + if !strings.Contains(text, atMention) { + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + postLog.Debug("Napcat WS message ignored (no @mention): " + text) + } + return "" // Not mentioned, ignore. + } + text = strings.ReplaceAll(text, atMention, "") + } + + // First check whether the received message is a command. + parsed, ok := controller.ParseCommand(text) + if !ok { + return "" // Not a command, discard. + } + + parsed.ChatID = cmd.ChatID + parsed.ChatType = cmd.ChatType + parsed.SenderID = cmd.SenderID + + // Hand the complete command to the unified processor, which checks group + // vs private, trusted groups, admin permissions, and executes it. + response, err := controller.GetManager().RouteCommand(parsed, q.cfg.TrustedGroups, q.cfg.Admins, q.cfg.ListenMethod) + if err != nil { + postLog.Error("Napcat command processing failed: " + err.Error()) + return "" + } + return response +} + // isSelfMessage returns true when the event was generated by the bot itself. func (q *QQController) isSelfMessage(ev oneBotEvent) bool { if q.cfg.BotQQID > 0 && ev.SelfID == q.cfg.BotQQID { @@ -156,210 +209,6 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool { return false } -// HandleCommand processes a bot command and returns a response string. -func (q *QQController) HandleCommand(cmd controller.Command) (string, error) { - cfg := config.GetConfig() - - // Debug: when ShowNapcatMsg is enabled, echo the received message back in real-time. - if cfg.Debug.ShowNapcatMsg { - postLog.Debug("Napcat message received: " + cmd.RawText) - return cmd.RawText, nil - } - - text := cmd.RawText - - // Check listen method. In "at" mode the @mention must be stripped before - // parsing, otherwise raw_message like "[CQ:at,qq=123] /list" fails the - // "/" prefix check. - if q.cfg.ListenMethod == "at" { - atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID) - if !strings.Contains(text, atMention) { - return "", nil // Not mentioned, ignore. - } - text = strings.ReplaceAll(text, atMention, "") - } - - parsed, ok := controller.ParseCommand(text) - if !ok { - // Not a command. In "global" mode, silently ignore. - // In "at" mode, this would be an error - but at detection happens at the message level. - return "", nil - } - - parsed.ChatID = cmd.ChatID - parsed.ChatType = cmd.ChatType - parsed.SenderID = cmd.SenderID - - return q.executeCommand(parsed) -} - -func (q *QQController) executeCommand(cmd controller.Command) (string, error) { - // Validate against supported commands. - switch cmd.Command { - case "list": - return q.handleList() - case "status": - return q.handleStatus(cmd) - case "shutdown": - return q.handleShutdown(cmd) - case "reboot": - return q.handleReboot(cmd) - case "run": - return q.handleRun(cmd) - default: - if q.cfg.ListenMethod == "at" { - return "Unknown command: /" + cmd.Command, nil - } - return "", nil // Global mode: silently ignore unknown commands. - } -} - -func (q *QQController) isAdmin(senderID int64) bool { - senderStr := fmt.Sprintf("%d", senderID) - for _, admin := range q.cfg.Admins { - if admin == senderStr { - return true - } - } - return false -} - -func (q *QQController) handleList() (string, error) { - cfg := config.GetConfig() - params := template.BuildParamsFromServerList() - return template.Render(cfg.ControllerMessage.ServerList, params), nil -} - -func (q *QQController) handleStatus(cmd controller.Command) (string, error) { - if len(cmd.Args) < 1 { - return "Usage: /status ", nil - } - uuid := cmd.Args[0] - tracker := node.GetTracker() - n, exists := tracker.GetNode(uuid) - if !exists { - return fmt.Sprintf("Server with UUID %s not found", uuid), nil - } - - statusStr := "Offline" - if n.Online { - statusStr = "Online" - } - - var sb strings.Builder - sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID)) - sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr)) - - if n.LatestReport != nil { - r := n.LatestReport - sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage)) - sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total)) - sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total)) - sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down)) - sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime)) - sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process)) - if r.Message != "" { - sb.WriteString(fmt.Sprintf("Message: %s\n", r.Message)) - } - } - - return sb.String(), nil -} - -func (q *QQController) handleShutdown(cmd controller.Command) (string, error) { - if !q.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 1 { - return "Usage: /shutdown ", nil - } - - uuid := cmd.Args[0] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - _, err := client.ExecTask([]string{uuid}, "shutdown") - if err != nil { - return fmt.Sprintf("Error: %v", err), nil - } - - return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil -} - -func (q *QQController) handleReboot(cmd controller.Command) (string, error) { - if !q.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 1 { - return "Usage: /reboot ", nil - } - - uuid := cmd.Args[0] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - _, err := client.ExecTask([]string{uuid}, "reboot") - if err != nil { - return fmt.Sprintf("Error: %v", err), nil - } - - return fmt.Sprintf("Reboot command sent to server %s", uuid), nil -} - -func (q *QQController) handleRun(cmd controller.Command) (string, error) { - if !q.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 2 { - return "Usage: /run ", nil - } - - uuidArg := cmd.Args[0] - command := cmd.Args[1] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - var uuids []string - if uuidArg == "all" { - tracker := node.GetTracker() - for _, n := range tracker.GetAllNodes() { - uuids = append(uuids, n.UUID) - } - } else { - uuids = []string{uuidArg} - } - - taskID, err := client.ExecTask(uuids, command) - if err != nil { - return fmt.Sprintf("Error executing command: %v", err), nil - } - - results, err := client.PollTaskResult(taskID) - if err != nil { - return fmt.Sprintf("Error getting results: %v", err), nil - } - - cfg := config.GetConfig() - params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results)) - return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil -} - -func formatTaskResults(results []komari.TaskResult) string { - var sb strings.Builder - for _, r := range results { - sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client)) - sb.WriteString(r.Result) - sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode)) - } - return sb.String() -} - // SendStatusChange sends a status change notification via QQ. func (q *QQController) SendStatusChange(change node.StatusChange) error { if !q.cfg.Enabled { diff --git a/internal/controller/pipes/telegram.go b/internal/controller/pipes/telegram.go index 5cb017c..6e2a69a 100644 --- a/internal/controller/pipes/telegram.go +++ b/internal/controller/pipes/telegram.go @@ -2,14 +2,12 @@ package pipes import ( "fmt" - "strings" "nukumizu-backend/config" - "nukumizu-backend/internal/komari" + "nukumizu-backend/internal/controller" "nukumizu-backend/internal/node" "nukumizu-backend/internal/template" "nukumizu-backend/postLog" - "nukumizu-backend/internal/controller" ) // TelegramController handles Telegram Bot interactions via long polling. @@ -70,184 +68,28 @@ func (t *TelegramController) pollLoop() { postLog.Info("Telegram polling started") } -// HandleCommand processes a bot command and returns a response string. -func (t *TelegramController) HandleCommand(cmd controller.Command) (string, error) { +// processCommand validates an incoming message as a command and hands the +// complete command to the unified processor. It returns the response text to +// reply with; an empty response means the message was discarded. +func (t *TelegramController) processCommand(cmd controller.Command) string { + // First check whether the received message is a command. parsed, ok := controller.ParseCommand(cmd.RawText) if !ok { - if t.cfg.ListenMethod == "at" { - return "Unknown command format. Use /command args", nil - } - return "", nil + return "" // Not a command, discard. } parsed.ChatID = cmd.ChatID parsed.ChatType = cmd.ChatType parsed.SenderID = cmd.SenderID - return t.executeCommand(parsed) -} - -func (t *TelegramController) executeCommand(cmd controller.Command) (string, error) { - switch cmd.Command { - case "list": - return t.handleList() - case "status": - return t.handleStatus(cmd) - case "shutdown": - return t.handleShutdown(cmd) - case "reboot": - return t.handleReboot(cmd) - case "run": - return t.handleRun(cmd) - default: - if t.cfg.ListenMethod == "at" { - return "Unknown command: /" + cmd.Command, nil - } - return "", nil - } -} - -func (t *TelegramController) isAdmin(senderID int64) bool { - senderStr := fmt.Sprintf("%d", senderID) - for _, admin := range t.cfg.Admins { - if admin == senderStr { - return true - } - } - return false -} - -func (t *TelegramController) handleList() (string, error) { - cfg := config.GetConfig() - params := template.BuildParamsFromServerList() - return template.Render(cfg.ControllerMessage.ServerList, params), nil -} - -func (t *TelegramController) handleStatus(cmd controller.Command) (string, error) { - if len(cmd.Args) < 1 { - return "Usage: /status ", nil - } - uuid := cmd.Args[0] - tracker := node.GetTracker() - n, exists := tracker.GetNode(uuid) - if !exists { - return fmt.Sprintf("Server with UUID %s not found", uuid), nil - } - - statusStr := "Offline" - if n.Online { - statusStr = "Online" - } - - var sb strings.Builder - sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID)) - sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr)) - - if n.LatestReport != nil { - r := n.LatestReport - sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage)) - sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total)) - sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total)) - sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down)) - sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime)) - sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process)) - } - - return sb.String(), nil -} - -func (t *TelegramController) handleShutdown(cmd controller.Command) (string, error) { - if !t.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 1 { - return "Usage: /shutdown ", nil - } - - uuid := cmd.Args[0] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - _, err := client.ExecTask([]string{uuid}, "shutdown") + // Hand the complete command to the unified processor, which checks group + // vs private, trusted groups, admin permissions, and executes it. + response, err := controller.GetManager().RouteCommand(parsed, t.cfg.TrustedGroups, t.cfg.Admins, t.cfg.ListenMethod) if err != nil { - return fmt.Sprintf("Error: %v", err), nil + postLog.Error("Telegram command processing failed: " + err.Error()) + return "" } - - return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil -} - -func (t *TelegramController) handleReboot(cmd controller.Command) (string, error) { - if !t.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 1 { - return "Usage: /reboot ", nil - } - - uuid := cmd.Args[0] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - _, err := client.ExecTask([]string{uuid}, "reboot") - if err != nil { - return fmt.Sprintf("Error: %v", err), nil - } - - return fmt.Sprintf("Reboot command sent to server %s", uuid), nil -} - -func (t *TelegramController) handleRun(cmd controller.Command) (string, error) { - if !t.isAdmin(cmd.SenderID) { - return "Permission denied: admin only", nil - } - if len(cmd.Args) < 2 { - return "Usage: /run ", nil - } - - uuidArg := cmd.Args[0] - command := cmd.Args[1] - client := komari.GetClient() - if client == nil { - return "Error: Komari client not initialized", nil - } - - var uuids []string - if uuidArg == "all" { - tracker := node.GetTracker() - for _, n := range tracker.GetAllNodes() { - uuids = append(uuids, n.UUID) - } - } else { - uuids = []string{uuidArg} - } - - taskID, err := client.ExecTask(uuids, command) - if err != nil { - return fmt.Sprintf("Error executing command: %v", err), nil - } - - results, err := client.PollTaskResult(taskID) - if err != nil { - return fmt.Sprintf("Error getting results: %v", err), nil - } - - cfg := config.GetConfig() - params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTelegramResults(results)) - return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil -} - -func formatTelegramResults(results []komari.TaskResult) string { - var sb strings.Builder - for _, r := range results { - sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client)) - sb.WriteString(r.Result) - sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode)) - } - return sb.String() + return response } // SendStatusChange sends a status change notification via Telegram. diff --git a/internal/controller/processor.go b/internal/controller/processor.go new file mode 100644 index 0000000..47a21f1 --- /dev/null +++ b/internal/controller/processor.go @@ -0,0 +1,198 @@ +package controller + +import ( + "fmt" + "strings" + + "nukumizu-backend/config" + "nukumizu-backend/internal/komari" + "nukumizu-backend/internal/node" + "nukumizu-backend/internal/template" + "nukumizu-backend/postLog" +) + +// RouteCommand validates an incoming command against the pipe's authorization +// config (trusted groups / admins) and, if authorized, processes it through +// Trigger. It returns the response text to reply with; an empty response means +// the command was discarded. +// +// All command handling across pipes is unified here: pipes only decide whether +// a received message is a command, then hand the complete command over. +func (m *Manager) RouteCommand(cmd Command, trustedGroups, admins []string, listenMethod string) (string, error) { + // Check whether the command arrived in a group or a private chat. + if cmd.ChatType == "group" { + // Group commands are only honored from trusted groups, otherwise discard. + if !IsTrustedGroup(cmd.ChatID, trustedGroups) { + return "", nil + } + } + + // Commands that require special permission must be sent by an admin. + if IsAdminCommand(cmd.Command) { + if !IsAdmin(cmd.SenderID, admins) { + return "", nil + } + } + + response, err := m.Trigger(cmd) + if err != nil { + return "", err + } + + // In "at" listen mode, give feedback for unknown commands. + if response == "" && listenMethod == "at" { + return "Unknown command: /" + cmd.Command, nil + } + return response, nil +} + +// Trigger processes a parsed bot command and returns the response text. +// The actual command execution for every pipe is unified here. +func (m *Manager) Trigger(cmd Command) (string, error) { + cfg := config.GetConfig() + if cfg.System.DebugMode && cfg.Debug.ShowTriggerCmdEcho { + postLog.Debug("Trigger received command: /" + cmd.Command + " with args: " + strings.Join(cmd.Args, ", ")) + } + + switch cmd.Command { + case "list": + return handleList() + case "status": + return handleStatus(cmd) + case "shutdown": + return handleShutdown(cmd) + case "reboot": + return handleReboot(cmd) + case "run": + return handleRun(cmd) + default: + return "", nil + } +} + +func handleList() (string, error) { + cfg := config.GetConfig() + params := template.BuildParamsFromServerList() + return template.Render(cfg.ControllerMessage.ServerList, params), nil +} + +func handleStatus(cmd Command) (string, error) { + if len(cmd.Args) < 1 { + return "Usage: /status ", nil + } + uuid := cmd.Args[0] + tracker := node.GetTracker() + n, exists := tracker.GetNode(uuid) + if !exists { + return fmt.Sprintf("Server with UUID %s not found", uuid), nil + } + + statusStr := "Offline" + if n.Online { + statusStr = "Online" + } + + var sb strings.Builder + sb.WriteString(fmt.Sprintf("Server: %s (%s)\n", n.Name, n.UUID)) + sb.WriteString(fmt.Sprintf("Status: %s\n", statusStr)) + + if n.LatestReport != nil { + r := n.LatestReport + sb.WriteString(fmt.Sprintf("CPU: %.2f%%\n", r.CPU.Usage)) + sb.WriteString(fmt.Sprintf("RAM: %d / %d\n", r.RAM.Used, r.RAM.Total)) + sb.WriteString(fmt.Sprintf("Disk: %d / %d\n", r.Disk.Used, r.Disk.Total)) + sb.WriteString(fmt.Sprintf("Network: ↑%d ↓%d\n", r.Network.Up, r.Network.Down)) + sb.WriteString(fmt.Sprintf("Uptime: %d seconds\n", r.Uptime)) + sb.WriteString(fmt.Sprintf("Processes: %d\n", r.Process)) + if r.Message != "" { + sb.WriteString(fmt.Sprintf("Message: %s\n", r.Message)) + } + } + + return sb.String(), nil +} + +func handleShutdown(cmd Command) (string, error) { + if len(cmd.Args) < 1 { + return "Usage: /shutdown ", nil + } + + uuid := cmd.Args[0] + client := komari.GetClient() + if client == nil { + return "Error: Komari client not initialized", nil + } + + _, err := client.ExecTask([]string{uuid}, "shutdown") + if err != nil { + return fmt.Sprintf("Error: %v", err), nil + } + + return fmt.Sprintf("Shutdown command sent to server %s", uuid), nil +} + +func handleReboot(cmd Command) (string, error) { + if len(cmd.Args) < 1 { + return "Usage: /reboot ", nil + } + + uuid := cmd.Args[0] + client := komari.GetClient() + if client == nil { + return "Error: Komari client not initialized", nil + } + + _, err := client.ExecTask([]string{uuid}, "reboot") + if err != nil { + return fmt.Sprintf("Error: %v", err), nil + } + + return fmt.Sprintf("Reboot command sent to server %s", uuid), nil +} + +func handleRun(cmd Command) (string, error) { + if len(cmd.Args) < 2 { + return "Usage: /run ", nil + } + + uuidArg := cmd.Args[0] + command := cmd.Args[1] + client := komari.GetClient() + if client == nil { + return "Error: Komari client not initialized", nil + } + + var uuids []string + if uuidArg == "all" { + tracker := node.GetTracker() + for _, n := range tracker.GetAllNodes() { + uuids = append(uuids, n.UUID) + } + } else { + uuids = []string{uuidArg} + } + + taskID, err := client.ExecTask(uuids, command) + if err != nil { + return fmt.Sprintf("Error executing command: %v", err), nil + } + + results, err := client.PollTaskResult(taskID) + if err != nil { + return fmt.Sprintf("Error getting results: %v", err), nil + } + + cfg := config.GetConfig() + params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results)) + return template.Render(cfg.ControllerMessage.ServerExecuteResult, params), nil +} + +func formatTaskResults(results []komari.TaskResult) string { + var sb strings.Builder + for _, r := range results { + sb.WriteString(fmt.Sprintf("--- %s ---\n", r.Client)) + sb.WriteString(r.Result) + sb.WriteString(fmt.Sprintf("\nExit code: %d\n", r.ExitCode)) + } + return sb.String() +} diff --git a/internal/controller/trigger.go b/internal/controller/trigger.go index 61dc5be..8f13f6a 100644 --- a/internal/controller/trigger.go +++ b/internal/controller/trigger.go @@ -1,57 +1,70 @@ -package controller - -import ( - "strings" - - "nukumizu-backend/config" - "nukumizu-backend/postLog" -) - -// Trigger is a placeholder for triggering commands across all controllers. -func (m *Manager) Trigger (command string, args []string) { - if config.GetConfig().System.DebugMode { - postLog.Debug("Received command: " + command + " with args: " + strings.Join(args, ", ")) - } -} - -// ParseCommand parses a raw message text into a Command. -// Format: /{{command}} {{args...}} -func ParseCommand(rawText string) (cmd Command, ok bool) { - rawText = strings.TrimSpace(rawText) - if !strings.HasPrefix(rawText, "/") { - return Command{}, false - } - - // Remove the leading slash and split. - parts := strings.SplitN(rawText[1:], " ", 2) - cmd.Command = strings.ToLower(parts[0]) - cmd.RawText = rawText - - if len(parts) > 1 { - argStr := strings.TrimSpace(parts[1]) - - // Special handling for /run: first arg is uuid, rest is command. - if cmd.Command == "run" { - spaceIdx := strings.Index(argStr, " ") - if spaceIdx > 0 { - cmd.Args = []string{argStr[:spaceIdx], strings.TrimSpace(argStr[spaceIdx+1:])} - } else { - cmd.Args = []string{argStr} - } - } else { - cmd.Args = strings.Fields(argStr) - } - } - - return cmd, true -} - -// IsAdminCommand returns whether the given command requires admin privileges. -func IsAdminCommand(command string) bool { - switch command { - case "shutdown", "reboot", "run": - return true - default: - return false - } -} +package controller + +import ( + "fmt" + "strings" +) + +// ParseCommand parses a raw message text into a Command. +// Format: /{{command}} {{args...}} +func ParseCommand(rawText string) (cmd Command, ok bool) { + rawText = strings.TrimSpace(rawText) + if !strings.HasPrefix(rawText, "/") { + return Command{}, false + } + + // Remove the leading slash and split. + parts := strings.SplitN(rawText[1:], " ", 2) + cmd.Command = strings.ToLower(parts[0]) + cmd.RawText = rawText + + if len(parts) > 1 { + argStr := strings.TrimSpace(parts[1]) + + // Special handling for /run: first arg is uuid, rest is command. + if cmd.Command == "run" { + spaceIdx := strings.Index(argStr, " ") + if spaceIdx > 0 { + cmd.Args = []string{argStr[:spaceIdx], strings.TrimSpace(argStr[spaceIdx+1:])} + } else { + cmd.Args = []string{argStr} + } + } else { + cmd.Args = strings.Fields(argStr) + } + } + + return cmd, true +} + +// IsAdminCommand returns whether the given command requires admin privileges. +func IsAdminCommand(command string) bool { + switch command { + case "shutdown", "reboot", "run": + return true + default: + return false + } +} + +// IsAdmin returns whether the sender is present in the given admins list. +func IsAdmin(senderID int64, admins []string) bool { + senderStr := fmt.Sprintf("%d", senderID) + for _, admin := range admins { + if admin == senderStr { + return true + } + } + return false +} + +// IsTrustedGroup returns whether the chat ID is present in the given trusted groups list. +func IsTrustedGroup(chatID int64, trustedGroups []string) bool { + chatStr := fmt.Sprintf("%d", chatID) + for _, group := range trustedGroups { + if group == chatStr { + return true + } + } + return false +}