From f2cc16177c280e4438779c592e539656820553cd Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Thu, 13 Aug 2026 17:07:55 +0800 Subject: [PATCH] feat(controller): add source for command handling. feat(telegram): switch message type to markdown feat(controller): add welcome message for telegram bot. --- config/config.go | 3 + config/variables.go | 1 + internal/controller/controller.go | 3 +- internal/controller/pipes/qq_napcat/qq.go | 4 +- internal/controller/pipes/telegram/client.go | 12 ++-- .../controller/pipes/telegram/telegram.go | 4 ++ internal/controller/processor.go | 64 ++----------------- internal/controller/trigger.go | 64 +++++++++++++++++++ 8 files changed, 90 insertions(+), 65 deletions(-) diff --git a/config/config.go b/config/config.go index a2ffdbf..b07e81c 100644 --- a/config/config.go +++ b/config/config.go @@ -94,6 +94,9 @@ func LoadConfig(configPath string) (*Config, error) { if cfg.ControllerMessage.BotHelp == "" { cfg.ControllerMessage.BotHelp = "Available commands:\n/help - Show this help message\n/list - List all servers\n/status - Show status of a specific server\n/shutdown - Shutdown a specific server\n/reboot - Reboot a specific server\n/run - Run a command on a specific server\n/info - Show bot information" } + if cfg.ControllerMessage.Tg_BotStart == "" { + cfg.ControllerMessage.Tg_BotStart = "Welcome to use Nukumizu Alert Bot!\nUse /help to see available commands." + } if cfg.ControllerMessage.ServerStatusChanged == "" { cfg.ControllerMessage.ServerStatusChanged = "Server Status Changed Alert\n{{ serverName }} - {{ upStatus }}\nEvent: {{ event }}\nServer Name: {{ serverName }}\nMessage: {{ message }}\nTime: {{ time }}" } diff --git a/config/variables.go b/config/variables.go index c9e9653..a1f2e43 100644 --- a/config/variables.go +++ b/config/variables.go @@ -99,6 +99,7 @@ type ControllerMethodConfig struct { type ControllerMessageConfig struct { BotStarted string `json:"BOT_STARTED"` BotHelp string `json:"BOT_HELP"` + Tg_BotStart string `json:"TG_BOT_START"` ServerStatusChanged string `json:"SERVER_STATUS_CHANGED"` ServerList string `json:"SERVER_LIST"` ServerExecuteResult string `json:"SERVER_EXECUTE_RESULT"` diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 68d9a24..d70d880 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -12,7 +12,8 @@ import ( // Command represents a parsed bot command. type Command struct { - RawText string + Source string // The source pipe (e.g., "telegram", "qq", "napcat") + RawText string // The raw text of the command message Command string // The command word (e.g., "list", "status") Args []string // Command arguments ChatID int64 // Chat/group ID where the command was issued diff --git a/internal/controller/pipes/qq_napcat/qq.go b/internal/controller/pipes/qq_napcat/qq.go index 4b30da2..88ece1d 100644 --- a/internal/controller/pipes/qq_napcat/qq.go +++ b/internal/controller/pipes/qq_napcat/qq.go @@ -179,12 +179,12 @@ func (q *QQController) processCommand(cmd controller.Command) string { parsed.ChatID = cmd.ChatID parsed.ChatType = cmd.ChatType parsed.SenderID = cmd.SenderID + parsed.Source = "qq_napcat" // Hand the complete command to the unified processor, which checks group // vs private, trusted groups, admin permissions, and executes it. response, err := controller.GetManager().Trigger(parsed, q.cfg.TrustedGroups, q.cfg.Admins, q.cfg.ListenMethod) - cfg := config.GetConfig() - if cfg.System.DebugMode && cfg.Debug.ShowTriggerCmdEcho { + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowTriggerCmdEcho { postLog.Debug(fmt.Sprintf("[qq_napcat] triggered command: \"/%s\" with args: \"%s\" from chatID: %d and senderID: %d", parsed.Command, strings.Join(parsed.Args, ", "), cmd.ChatID, cmd.SenderID)) } if err != nil { diff --git a/internal/controller/pipes/telegram/client.go b/internal/controller/pipes/telegram/client.go index ff699f0..dd1ceb5 100644 --- a/internal/controller/pipes/telegram/client.go +++ b/internal/controller/pipes/telegram/client.go @@ -182,8 +182,11 @@ func (c *Client) getUpdates(offset int64, limit, timeout int) ([]Update, error) } // SendMessage sends a text message to a chat, splitting it into chunks that -// fit Telegram's 4096-character limit. The text is sent without a parse mode so -// template asterisks render literally. +// fit Telegram's 4096-character limit. All messages are sent with +// parse_mode=Markdown so fenced code blocks and inline formatting render as +// rich text. Templates must stay valid under Telegram's legacy Markdown: +// unpaired '*' or '_' characters (e.g. a lone '*Event: ...' label) make the +// API reject the whole message. func (c *Client) SendMessage(chatID int64, text string) error { if strings.TrimSpace(text) == "" { return nil @@ -198,8 +201,9 @@ func (c *Client) SendMessage(chatID int64, text string) error { func (c *Client) sendMessageChunk(chatID int64, text string) error { params := url.Values{ - "chat_id": {strconv.FormatInt(chatID, 10)}, - "text": {text}, + "chat_id": {strconv.FormatInt(chatID, 10)}, + "text": {text}, + "parse_mode": {"Markdown"}, } return c.call("sendMessage", params, nil) } diff --git a/internal/controller/pipes/telegram/telegram.go b/internal/controller/pipes/telegram/telegram.go index cc51129..5fd6954 100644 --- a/internal/controller/pipes/telegram/telegram.go +++ b/internal/controller/pipes/telegram/telegram.go @@ -169,10 +169,14 @@ func (t *TelegramController) processCommand(cmd controller.Command) string { parsed.ChatID = cmd.ChatID parsed.ChatType = cmd.ChatType parsed.SenderID = cmd.SenderID + parsed.Source = "telegram" // Hand the complete command to the unified processor, which checks group // vs private, trusted groups, admin permissions, and executes it. response, err := controller.GetManager().Trigger(parsed, t.cfg.TrustedGroups, t.resolvedAdminList(), t.cfg.ListenMethod) + if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowTriggerCmdEcho { + postLog.Debug(fmt.Sprintf("[telegram] triggered command: \"/%s\" with args: \"%s\" from chatID: %d and senderID: %d", parsed.Command, strings.Join(parsed.Args, ", "), cmd.ChatID, cmd.SenderID)) + } if err != nil { postLog.Error("Telegram command processing failed: " + err.Error()) return "" diff --git a/internal/controller/processor.go b/internal/controller/processor.go index 8ed8cf9..70fa184 100644 --- a/internal/controller/processor.go +++ b/internal/controller/processor.go @@ -10,64 +10,6 @@ import ( "nukumizu-backend/internal/template" ) -// Trigger validates an incoming command against the pipe's authorization -// config (trusted groups / admins) and, if authorized, processes it through -// RouteCommand. 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) Trigger(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.RouteCommand(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 -} - -// RouteCommand processes a parsed bot command and returns the response text. -// The actual command execution for every pipe is unified here. -func (m *Manager) RouteCommand(cmd Command) (string, error) { - switch cmd.Command { - case "help": - return handleHelp() - case "list": - return handleList() - case "status": - return handleStatus(cmd) - case "shutdown": - return handleShutdown(cmd) - case "reboot": - return handleReboot(cmd) - case "run": - return handleRun(cmd) - case "info": - return handleInfo(cmd) - default: - return "", nil - } -} - func handleHelp() (string, error) { cfg := config.GetConfig() params := template.BuildBotInitializationMsgParams() @@ -235,6 +177,12 @@ func handleInfo(cmd Command) (string, error) { return sb.String(), nil } +func telegram_handleStart() (string, error) { + cfg := config.GetConfig() + params := template.BuildBotInitializationMsgParams() + return template.Render(cfg.ControllerMessage.Tg_BotStart, params), nil +} + // formatBytes renders a byte count in a human-readable form. func formatBytes(b int64) string { const ( diff --git a/internal/controller/trigger.go b/internal/controller/trigger.go index 8f13f6a..5ee6263 100644 --- a/internal/controller/trigger.go +++ b/internal/controller/trigger.go @@ -5,6 +5,70 @@ import ( "strings" ) +// Trigger validates an incoming command against the pipe's authorization +// config (trusted groups / admins) and, if authorized, processes it through +// RouteCommand. 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) Trigger(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.RouteCommand(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 +} + +// RouteCommand processes a parsed bot command and returns the response text. +// The actual command execution for every pipe is unified here. +func (m *Manager) RouteCommand(cmd Command) (string, error) { + if cmd.Source == "telegram"{ + switch cmd.Command { + case "start": + return telegram_handleStart() + } + } + switch cmd.Command { + case "help": + return handleHelp() + case "list": + return handleList() + case "status": + return handleStatus(cmd) + case "shutdown": + return handleShutdown(cmd) + case "reboot": + return handleReboot(cmd) + case "run": + return handleRun(cmd) + case "info": + return handleInfo(cmd) + default: + return "", nil + } +} + // ParseCommand parses a raw message text into a Command. // Format: /{{command}} {{args...}} func ParseCommand(rawText string) (cmd Command, ok bool) {