diff --git a/.gitignore b/.gitignore index 97f87c0..8df5077 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.claude agent.md config.json +bot_user_config.json *.exe # SQLite database files. They live on a network share (Y:), and git/cloud diff --git a/config/config.go b/config/config.go index b07e81c..ce4c3e0 100644 --- a/config/config.go +++ b/config/config.go @@ -6,9 +6,9 @@ import ( "os" ) -// LoadConfig reads and parses the configuration file, applies defaults, +// LoadGlobalConfig reads and parses the configuration file, applies defaults, // and stores it as a global singleton. -func LoadConfig(configPath string) (*Config, error) { +func LoadGlobalConfig(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) @@ -37,23 +37,11 @@ func LoadConfig(configPath string) (*Config, error) { if cfg.ControllerMethod.QQ.NapcatPort == "" { cfg.ControllerMethod.QQ.NapcatPort = "3000" } - if cfg.ControllerMethod.QQ.Admins == nil { - cfg.ControllerMethod.QQ.Admins = []string{} - } - if cfg.ControllerMethod.QQ.TrustedGroups == nil { - cfg.ControllerMethod.QQ.TrustedGroups = []string{} - } // Apply defaults for Telegram controller. if cfg.ControllerMethod.Telegram.ListenMethod == "" { cfg.ControllerMethod.Telegram.ListenMethod = "global" } - if cfg.ControllerMethod.Telegram.Admins == nil { - cfg.ControllerMethod.Telegram.Admins = []string{} - } - if cfg.ControllerMethod.Telegram.TrustedGroups == nil { - cfg.ControllerMethod.Telegram.TrustedGroups = []string{} - } // Apply defaults for Email controller. if cfg.ControllerMethod.Email.SMTPPort == 0 { @@ -107,19 +95,35 @@ func LoadConfig(configPath string) (*Config, error) { cfg.ControllerMessage.ServerExecuteResult = "Command execute result:\nServer Name: {{ serverName }}\nCommand: {{ command }}\n***Result***\n\n{{ result }}\n\n************\nTime: {{ time }}" } - globalConfig = &cfg + C_globalConfig = &cfg return &cfg, nil } -// GetConfig returns the global configuration singleton. -func GetConfig() *Config { - return globalConfig +// GetGlobalConfig returns the global configuration singleton. +func GetGlobalConfig() *Config { + return C_globalConfig +} + +// LoadBotUserConfig reads and parses bot_user_config.json and stores it as the +// global C_botUserConfig singleton, mirroring LoadGlobalConfig. +func LoadBotUserConfig(configPath string) (*BotUserConfig, error) { + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read bot user config file: %w", err) + } + + var cfg BotUserConfig + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("failed to parse bot user config file: %w", err) + } + C_botUserConfig = &cfg + return &cfg, nil } // IsDebugMode returns whether debug mode is enabled. func IsDebugMode() bool { - if globalConfig == nil { + if C_globalConfig == nil { return false } - return globalConfig.System.DebugMode + return C_globalConfig.System.DebugMode } diff --git a/config/variables.go b/config/variables.go index a1f2e43..c9d84ff 100644 --- a/config/variables.go +++ b/config/variables.go @@ -1,5 +1,7 @@ package config +import "sort" + // SystemConfig holds system-level configuration. type SystemConfig struct { DebugMode bool `json:"debugMode"` @@ -32,25 +34,21 @@ type KomariConfig struct { // QQConfig holds QQ (Napcat) Bot controller configuration. type QQConfig struct { - Enabled bool `json:"enabled"` - NetworkUseProxy bool `json:"networkUseProxy"` - NapcatAddr string `json:"napcatAddr"` - NapcatPort string `json:"napcatPort"` - NapcatToken string `json:"napcatToken"` - BotQQID int64 `json:"botQQID"` - ListenMethod string `json:"listenMethod"` - Admins []string `json:"admins"` - TrustedGroups []string `json:"trustedGroups"` + Enabled bool `json:"enabled"` + NetworkUseProxy bool `json:"networkUseProxy"` + NapcatAddr string `json:"napcatAddr"` + NapcatPort string `json:"napcatPort"` + NapcatToken string `json:"napcatToken"` + BotQQID int64 `json:"botQQID"` + ListenMethod string `json:"listenMethod"` } // TelegramConfig holds Telegram Bot controller configuration. type TelegramConfig struct { - Enabled bool `json:"enabled"` - NetworkUseProxy bool `json:"networkUseProxy"` - BotToken string `json:"botToken"` - ListenMethod string `json:"listenMethod"` - Admins []string `json:"admins"` - TrustedGroups []string `json:"trustedGroups"` + Enabled bool `json:"enabled"` + NetworkUseProxy bool `json:"networkUseProxy"` + BotToken string `json:"botToken"` + ListenMethod string `json:"listenMethod"` } // EmailConfig holds Email notification controller configuration. @@ -98,7 +96,7 @@ type ControllerMethodConfig struct { // ControllerMessageConfig holds message templates for controller responses. type ControllerMessageConfig struct { BotStarted string `json:"BOT_STARTED"` - BotHelp string `json:"BOT_HELP"` + BotHelp string `json:"BOT_HELP"` Tg_BotStart string `json:"TG_BOT_START"` ServerStatusChanged string `json:"SERVER_STATUS_CHANGED"` ServerList string `json:"SERVER_LIST"` @@ -108,7 +106,7 @@ type ControllerMessageConfig struct { // Config is the top-level application configuration. type Config struct { System SystemConfig `json:"system"` - Debug DebugConfig `json:"debug"` + Debug DebugConfig `json:"debug"` Komari KomariConfig `json:"komari"` ControllerMethod ControllerMethodConfig `json:"controllerMethod"` ControllerMessage ControllerMessageConfig `json:"controllerMessage"` @@ -116,4 +114,49 @@ type Config struct { DBPath string `json:"dbPath"` } -var globalConfig *Config \ No newline at end of file +var C_globalConfig *Config + +// BotUserOptions holds per-member options stored in bot_user_config.json. +type BotUserOptions struct { + // EventStatusNotify indicates whether this member is subscribed to node + // status change notifications. + EventStatusNotify bool `json:"event_status_notify"` +} + +// BotUserMembers maps a member ID (QQ number, Telegram @username or numeric +// user ID, or chat/group ID) to its per-member options. +type BotUserMembers map[string]BotUserOptions + +// IDs returns the member IDs as a sorted slice. JSON object keys have no +// stable iteration order once decoded into a map, so the result is sorted to +// keep notifications and authorization checks deterministic. +func (m BotUserMembers) IDs() []string { + ids := make([]string, 0, len(m)) + for id := range m { + ids = append(ids, id) + } + sort.Strings(ids) + return ids +} + +// BotUser_QQConfig holds the admins and trusted groups for the QQ (Napcat) bot. +type BotUser_QQConfig struct { + Admins BotUserMembers `json:"admins"` + TrustedGroups BotUserMembers `json:"trustedGroups"` +} + +// BotUser_TelegramConfig holds the admins and trusted groups for the Telegram bot. +type BotUser_TelegramConfig struct { + Admins BotUserMembers `json:"admins"` + TrustedGroups BotUserMembers `json:"trustedGroups"` +} + +// BotUserConfig is the schema of bot_user_config.json, which lists the admins +// and trusted groups (chat targets) for each bot channel separately from the +// main config.json. +type BotUserConfig struct { + QQ BotUser_QQConfig `json:"qq(napcat)"` + Telegram BotUser_TelegramConfig `json:"telegram"` +} + +var C_botUserConfig *BotUserConfig diff --git a/internal/controller/controller.go b/internal/controller/controller.go index d70d880..67a0167 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -77,7 +77,7 @@ func (m *Manager) ShowBotInitMessage() { m.mu.RLock() defer m.mu.RUnlock() - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildBotInitializationMsgParams() message := template.Render(cfg.ControllerMessage.BotStarted, params) @@ -102,7 +102,7 @@ func (m *Manager) ShowBotServerList() { m.mu.RLock() defer m.mu.RUnlock() - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() message := template.Render(cfg.ControllerMessage.ServerList, params) @@ -125,7 +125,7 @@ func (m *Manager) NotifyStatusChange(change node.StatusChange) { m.mu.RLock() defer m.mu.RUnlock() - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() templateStr := cfg.ControllerMessage.ServerStatusChanged params := template.BuildParamsFromStatusChange(change) diff --git a/internal/controller/pipes/email.go b/internal/controller/pipes/email.go index 07d3f13..b4f50e3 100644 --- a/internal/controller/pipes/email.go +++ b/internal/controller/pipes/email.go @@ -64,7 +64,7 @@ func (e *EmailController) SendStatusChange(change node.StatusChange) error { return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromStatusChange(change) body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params) @@ -78,7 +78,7 @@ func (e *EmailController) SendServerList(onlineServers, offlineServers string) e return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() body := template.Render(cfg.ControllerMessage.ServerList, params) @@ -91,7 +91,7 @@ func (e *EmailController) SendExecuteResult(serverName, serverUUID, command, res return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result) body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params) diff --git a/internal/controller/pipes/ntfy.go b/internal/controller/pipes/ntfy.go index ed5452d..bfa48cf 100644 --- a/internal/controller/pipes/ntfy.go +++ b/internal/controller/pipes/ntfy.go @@ -58,7 +58,7 @@ func (n *NtfyController) SendStatusChange(change node.StatusChange) error { return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromStatusChange(change) message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params) @@ -72,7 +72,7 @@ func (n *NtfyController) SendServerList(onlineServers, offlineServers string) er return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() message := template.Render(cfg.ControllerMessage.ServerList, params) @@ -85,7 +85,7 @@ func (n *NtfyController) SendExecuteResult(serverName, serverUUID, command, resu return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result) message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params) diff --git a/internal/controller/pipes/qq_napcat/napcat.go b/internal/controller/pipes/qq_napcat/napcat.go index 85278db..ae47207 100644 --- a/internal/controller/pipes/qq_napcat/napcat.go +++ b/internal/controller/pipes/qq_napcat/napcat.go @@ -155,7 +155,7 @@ func (c *Client) SendMsg(targetType string, targetID int64, msg string, hasAt bo return nil, fmt.Errorf("failed to marshal request: %w", err) } - if config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug(fmt.Sprintf("[Napcat] SendMsg -> %s (%s): %s", endpoint, targetType, message)) } @@ -176,7 +176,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - if config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug(fmt.Sprintf("[Napcat] RecallMsg -> /delete_msg: %d", msgID)) } @@ -190,7 +190,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) { // GetGroupList retrieves the list of joined groups from NapCat. func (c *Client) GetGroupList() (*APIResponse, error) { - if config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("[Napcat] GetGroupList -> /get_group_list") } @@ -211,7 +211,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) { return nil, fmt.Errorf("failed to marshal request: %w", err) } - if config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug(fmt.Sprintf("[Napcat] GetGroupInfo -> /get_group_info: %d", groupID)) } @@ -225,7 +225,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) { // GetFriendsList retrieves the friends list from NapCat. func (c *Client) GetFriendsList() (*APIResponse, error) { - if config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("[Napcat] GetFriendsList -> /get_friend_list") } diff --git a/internal/controller/pipes/qq_napcat/qq.go b/internal/controller/pipes/qq_napcat/qq.go index 88ece1d..32b6d47 100644 --- a/internal/controller/pipes/qq_napcat/qq.go +++ b/internal/controller/pipes/qq_napcat/qq.go @@ -15,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"` // 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 + 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"` // The bot's QQ ID - SubType string `json:"sub_type"` // The subtype of the message, e.g., "normal", "anonymous", etc. + 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. @@ -94,24 +94,24 @@ func (q *QQController) handleNapcatEvent(raw []byte) { return } - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatMsg { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowNapcatMsg { postLog.Debug("Napcat WS event received: " + string(raw)) } // Only handle message events; ignore notice/request/meta_event. if ev.PostType != "message" { - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("Ignoring Napcat WS event: " + string(raw)) } return } // Ignore messages the bot itself sent (echo prevention). - if q.isSelfMessage(ev) && !config.GetConfig().System.DebugMode { + if q.isSelfMessage(ev) && !config.GetGlobalConfig().System.DebugMode { return } - if q.isSelfMessage(ev) && config.GetConfig().System.DebugMode && config.GetConfig().Debug.NapcatIgnoreSelfMsg { - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + if q.isSelfMessage(ev) && config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.NapcatIgnoreSelfMsg { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("Ignoring Napcat WS self message: " + string(raw)) } return @@ -132,7 +132,7 @@ func (q *QQController) handleNapcatEvent(raw []byte) { response := q.processCommand(cmd) if response == "" { - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowNapcatAction { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("Napcat WS command discarded: " + string(raw)) } return @@ -162,7 +162,7 @@ func (q *QQController) processCommand(cmd controller.Command) string { 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 { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowNapcatAction { postLog.Debug("Napcat WS message ignored (no @mention): " + text) } return "" // Not mentioned, ignore. @@ -183,8 +183,8 @@ func (q *QQController) processCommand(cmd controller.Command) string { // 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) - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowTriggerCmdEcho { + response, err := controller.GetManager().Trigger(parsed, q.trustedGroupIDs(), q.adminIDs(), q.cfg.ListenMethod) + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().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 { @@ -205,6 +205,22 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool { return false } +// adminIDs returns the QQ admin IDs from bot_user_config.json. +func (q *QQController) adminIDs() []string { + if c := config.C_botUserConfig; c != nil { + return c.QQ.Admins.IDs() + } + return nil +} + +// trustedGroupIDs returns the QQ trusted group IDs from bot_user_config.json. +func (q *QQController) trustedGroupIDs() []string { + if c := config.C_botUserConfig; c != nil { + return c.QQ.TrustedGroups.IDs() + } + return nil +} + // SendMessage sends an arbitrary message (e.g. the bot initialization message) // to all QQ trusted groups and admins. func (q *QQController) SendMessage(message string) error { @@ -212,10 +228,10 @@ func (q *QQController) SendMessage(message string) error { return nil } - for _, groupID := range q.cfg.TrustedGroups { + for _, groupID := range q.trustedGroupIDs() { q.sendGroupMessage(groupID, message) } - for _, adminID := range q.cfg.Admins { + for _, adminID := range q.adminIDs() { q.sendPrivateMessage(adminID, message) } return nil @@ -227,17 +243,17 @@ func (q *QQController) SendStatusChange(change node.StatusChange) error { return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromStatusChange(change) message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params) // Send to trusted groups. - for _, groupID := range q.cfg.TrustedGroups { + for _, groupID := range q.trustedGroupIDs() { q.sendGroupMessage(groupID, message) } // Send to admins via private message. - for _, adminID := range q.cfg.Admins { + for _, adminID := range q.adminIDs() { q.sendPrivateMessage(adminID, message) } @@ -250,11 +266,11 @@ func (q *QQController) SendServerList(onlineServers, offlineServers string) erro return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() message := template.Render(cfg.ControllerMessage.ServerList, params) - for _, groupID := range q.cfg.TrustedGroups { + for _, groupID := range q.trustedGroupIDs() { q.sendGroupMessage(groupID, message) } return nil @@ -266,11 +282,11 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result) message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params) - for _, groupID := range q.cfg.TrustedGroups { + for _, groupID := range q.trustedGroupIDs() { q.sendGroupMessage(groupID, message) } return nil diff --git a/internal/controller/pipes/telegram/telegram.go b/internal/controller/pipes/telegram/telegram.go index 5353290..0798af7 100644 --- a/internal/controller/pipes/telegram/telegram.go +++ b/internal/controller/pipes/telegram/telegram.go @@ -142,7 +142,7 @@ func (t *TelegramController) handleUpdate(_ context.Context, _ *bot.Bot, update } msg := update.Message - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowTelegramMsg { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowTelegramMsg { raw, _ := json.Marshal(update) postLog.Debug("Telegram update received: " + string(raw)) } @@ -218,8 +218,8 @@ func (t *TelegramController) processCommand(cmd controller.Command) string { // 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 { + response, err := controller.GetManager().Trigger(parsed, t.trustedGroupIDs(), t.resolvedAdminList(), t.cfg.ListenMethod) + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().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 { @@ -245,7 +245,7 @@ func (t *TelegramController) SendStatusChange(change node.StatusChange) error { return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromStatusChange(change) message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params) @@ -259,7 +259,7 @@ func (t *TelegramController) SendServerList(onlineServers, offlineServers string return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() message := template.Render(cfg.ControllerMessage.ServerList, params) @@ -273,7 +273,7 @@ func (t *TelegramController) SendExecuteResult(serverName, serverUUID, command, return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result) message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params) @@ -334,13 +334,31 @@ func (t *TelegramController) resolveUsername(username string) (int64, bool) { return id, ok } +// adminIDs returns the Telegram admin entries (numeric user ID or @username) +// from bot_user_config.json. +func (t *TelegramController) adminIDs() []string { + if c := config.C_botUserConfig; c != nil { + return c.Telegram.Admins.IDs() + } + return nil +} + +// trustedGroupIDs returns the Telegram trusted group IDs from bot_user_config.json. +func (t *TelegramController) trustedGroupIDs() []string { + if c := config.C_botUserConfig; c != nil { + return c.Telegram.TrustedGroups.IDs() + } + return nil +} + // resolvedAdminList maps username-based admin entries to numeric IDs so the // unified processor's numeric IsAdmin check works. Unresolvable entries (the // user has not messaged the bot yet) pass through unchanged and simply never // match. func (t *TelegramController) resolvedAdminList() []string { - result := make([]string, 0, len(t.cfg.Admins)) - for _, admin := range t.cfg.Admins { + admins := t.adminIDs() + result := make([]string, 0, len(admins)) + for _, admin := range admins { if id, ok := t.resolveUsername(admin); ok { result = append(result, strconv.FormatInt(id, 10)) } else { @@ -353,14 +371,14 @@ func (t *TelegramController) resolvedAdminList() []string { // sendToGroupsAndAdmins sends a message to all trusted groups and admins. func (t *TelegramController) sendToGroupsAndAdmins(message string) { t.sendToGroups(message) - for _, admin := range t.cfg.Admins { + for _, admin := range t.adminIDs() { t.sendAdminMessage(admin, message) } } // sendToGroups sends a message to all trusted groups. func (t *TelegramController) sendToGroups(message string) { - for _, groupID := range t.cfg.TrustedGroups { + for _, groupID := range t.trustedGroupIDs() { t.sendGroupMessage(groupID, message) } } diff --git a/internal/controller/pipes/webhook.go b/internal/controller/pipes/webhook.go index f3485a8..5793b46 100644 --- a/internal/controller/pipes/webhook.go +++ b/internal/controller/pipes/webhook.go @@ -59,7 +59,7 @@ func (w *WebhookController) SendStatusChange(change node.StatusChange) error { return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromStatusChange(change) message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params) @@ -80,7 +80,7 @@ func (w *WebhookController) SendServerList(onlineServers, offlineServers string) return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() message := template.Render(cfg.ControllerMessage.ServerList, params) @@ -101,7 +101,7 @@ func (w *WebhookController) SendExecuteResult(serverName, serverUUID, command, r return nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result) message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params) diff --git a/internal/controller/processor.go b/internal/controller/processor.go index b270778..cc50533 100644 --- a/internal/controller/processor.go +++ b/internal/controller/processor.go @@ -11,13 +11,13 @@ import ( ) func handleHelp(cmd Command) (string, error) { - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildBotInitializationMsgParams() return template.Render(cfg.ControllerMessage.BotHelp, params, cmd.Source), nil } func handleList(cmd Command) (string, error) { - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromServerList() return template.Render(cfg.ControllerMessage.ServerList, params, cmd.Source), nil } @@ -128,7 +128,7 @@ func handleRun(cmd Command) (string, error) { return fmt.Sprintf("Error getting results: %v", err), nil } - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results)) return template.Render(cfg.ControllerMessage.ServerExecuteResult, params, cmd.Source), nil } @@ -178,7 +178,7 @@ func handleInfo(cmd Command) (string, error) { } func telegram_handleStart() (string, error) { - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() params := template.BuildBotInitializationMsgParams() return template.Render(cfg.ControllerMessage.Tg_BotStart, params, "telegram"), nil } diff --git a/internal/komari/client.go b/internal/komari/client.go index af18d76..07f35fa 100644 --- a/internal/komari/client.go +++ b/internal/komari/client.go @@ -145,7 +145,7 @@ func (c *Client) Login(username, password string) error { var kr KomariResponse if err := json.NewDecoder(resp.Body).Decode(&kr); err != nil { - if config.GetConfig().System.DebugMode { + if config.GetGlobalConfig().System.DebugMode { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, respBody) } @@ -317,7 +317,7 @@ func (c *Client) ExecTask(uuids []string, command string) (string, error) { return "", fmt.Errorf("failed to parse komari task exec data: %w", err) } - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowKomariTaskEcho { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowKomariTaskEcho { postLog.Debug(fmt.Sprintf("Created Komari task %s for %d clients", result.TaskID, len(uuids))) } return result.TaskID, nil @@ -360,7 +360,7 @@ func (c *Client) GetTaskResult(taskID string) ([]TaskResult, bool, error) { // PollTaskResult polls for task results every 1 second until all results are // available or 60 seconds have elapsed. func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) { - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowKomariTaskEcho { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowKomariTaskEcho { postLog.Debug(fmt.Sprintf("Polling for Komari task %s results...", taskID)) } @@ -378,7 +378,7 @@ func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) { return nil, err } if done { - if config.GetConfig().System.DebugMode && config.GetConfig().Debug.ShowKomariTaskEcho { + if config.GetGlobalConfig().System.DebugMode && config.GetGlobalConfig().Debug.ShowKomariTaskEcho { postLog.Info(fmt.Sprintf("Task %s completed with %d results", taskID, len(results))) } return results, nil diff --git a/internal/komari/ws.go b/internal/komari/ws.go index 850e49e..a32217c 100644 --- a/internal/komari/ws.go +++ b/internal/komari/ws.go @@ -409,7 +409,7 @@ func GetWSClient() *WSClient { // LoginAndStart performs the Komari login and returns an error if it fails. func LoginAndStart() error { - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() client := GetClient() if client == nil { return fmt.Errorf("komari client not initialized") diff --git a/internal/netproxy/netproxy.go b/internal/netproxy/netproxy.go index d52d4c6..098fbba 100644 --- a/internal/netproxy/netproxy.go +++ b/internal/netproxy/netproxy.go @@ -20,7 +20,7 @@ import ( // proxyURL returns the system-wide network proxy URL, or nil when none is // configured. A missing scheme is normalized to http:// for convenience. func proxyURL() *url.URL { - raw := config.GetConfig().System.NetworkProxy + raw := config.GetGlobalConfig().System.NetworkProxy if raw == "" { return nil } diff --git a/main.go b/main.go index 3d5a9ed..a572cdd 100644 --- a/main.go +++ b/main.go @@ -35,16 +35,22 @@ func main() { global.SoftwareInfo.BuildTime = BuildTime // Parse CLI flags. - configPath := flag.String("config", "config.json", "Path to configuration file") + configPath_global := flag.String("config", "config.json", "Path to configuration file") + configPath_bot_user := flag.String("bot-user-config", "bot_user_config.json", "Path to bot user configuration file") flag.Parse() // Log startup banner. postLog.Info(fmt.Sprintf("%s Ver.%s.%d.%s.%s Developed by %s at %s", global.SoftwareInfo.Name, global.SoftwareInfo.Version, global.SoftwareInfo.BuildVer, global.SoftwareInfo.BuildType, global.SoftwareInfo.CommitHash, global.SoftwareInfo.Developer, global.SoftwareInfo.BuildTime)) // Load configuration. - cfg, err := config.LoadConfig(*configPath) + cfg, err := config.LoadGlobalConfig(*configPath_global) if err != nil { - log.Fatalf("Failed to load config: %v", err) + log.Fatalf("Failed to load global config: %v", err) + } + + _, err = config.LoadBotUserConfig(*configPath_bot_user) + if err != nil { + log.Fatalf("Failed to load bot user config: %v", err) } // Initialize logging. @@ -162,7 +168,7 @@ func main() { // initControllers initializes and starts all configured controllers. func initControllers() { - cfg := config.GetConfig() + cfg := config.GetGlobalConfig() mgr := controller.GetManager() if mgr == nil { return