7 changed files with 122 additions and 20 deletions
+1
View File
@@ -2,6 +2,7 @@
agent.md
config.json
bot_user_config.json
bot_node_config.json
*.exe
nukumizu-linux-amd64
+64
View File
@@ -1,11 +1,31 @@
package config
import (
"bytes"
"encoding/json"
"fmt"
"os"
"sync"
)
// botNodeConfigMu guards botNodeConfig, the in-memory mirror of
// bot_node_config.json.
var (
botNodeConfigMu sync.RWMutex
botNodeConfig BotNodeMembers
)
// NodeStatusNotifyEnabled reports whether the node identified by uuid should
// broadcast status-change notifications, per bot_node_config.json. The flag is
// allow-list only: a node notifies when its entry has enableStatusNotify set to
// true, and stays silent when the entry is absent or not enabled.
func NodeStatusNotifyEnabled(uuid string) bool {
botNodeConfigMu.RLock()
opts, ok := botNodeConfig[uuid]
botNodeConfigMu.RUnlock()
return ok && opts.EnableStatusNotify
}
// LoadGlobalConfig reads and parses the configuration file, applies defaults,
// and stores it as a global singleton.
func LoadGlobalConfig(configPath string) (*Config, error) {
@@ -115,6 +135,50 @@ func LoadBotUserConfig(configPath string) (*BotUserConfig, error) {
return &cfg, nil
}
// SaveBotNodeConfig persists the given node UUIDs to the bot node config file
// and refreshes the in-memory mirror read by NodeStatusNotifyEnabled. Entries
// already present in the file are always preserved: a UUID Komari no longer
// reports on a given fetch is kept rather than deleted, so per-node settings
// for stale nodes survive a node-list refresh that does not include them. UUIDs
// seen for the first time are added disabled (enableStatusNotify defaults to
// false); enable a node's status notifications by setting the flag to true in
// the file. The resulting JSON has its object keys emitted in sorted order by
// encoding/json, keeping the file deterministic across writes. The path is
// supplied by the caller (typically global.ConfigPath.BotNodeConfig).
func SaveBotNodeConfig(configPath string, uuids []string) error {
// Start from whatever is already on disk so nothing is dropped. An empty or
// missing file is treated as an empty map.
members := make(BotNodeMembers)
if data, err := os.ReadFile(configPath); err == nil && len(bytes.TrimSpace(data)) > 0 {
if err := json.Unmarshal(data, &members); err != nil {
return fmt.Errorf("failed to parse existing bot node config %s: %w", configPath, err)
}
}
// Add every currently-fetched UUID, but never overwrite an entry that is
// already configured.
for _, uuid := range uuids {
if _, ok := members[uuid]; !ok {
members[uuid] = BotNodeOptions{}
}
}
data, err := json.MarshalIndent(members, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal bot node config: %w", err)
}
data = append(data, '\n')
if err := os.WriteFile(configPath, data, 0o644); err != nil {
return fmt.Errorf("failed to write bot node config %s: %w", configPath, err)
}
botNodeConfigMu.Lock()
botNodeConfig = members
botNodeConfigMu.Unlock()
return nil
}
// IsDebugMode returns whether debug mode is enabled.
func IsDebugMode() bool {
if C_globalConfig == nil {
+16
View File
@@ -170,3 +170,19 @@ type BotUserConfig struct {
}
var C_botUserConfig *BotUserConfig
// BotNodeOptions holds per-node options stored in bot_node_config.json. The
// file is auto-populated by the node tracker for every node Komari reports;
// per-node options are edited by hand in the JSON file.
type BotNodeOptions struct {
// EnableStatusNotify is an allow-list switch: a node broadcasts status-change
// notifications only when this is true. Nodes default to false (silent)
// unless explicitly enabled.
EnableStatusNotify bool `json:"enableStatusNotify"`
}
// BotNodeMembers maps a node UUID (as reported by Komari) to its per-node
// options, e.g. bot_node_config.json:
//
// {"<uuid1>": {"enableStatusNotify": true}, "<uuid2>": {}}
type BotNodeMembers map[string]BotNodeOptions
+15 -3
View File
@@ -14,11 +14,23 @@ type SoftwareInfoStr struct {
var SoftwareInfo = SoftwareInfoStr{
Name: "Nukumizu",
Version: "0.1.0",
Version: "0.1.1",
Developer: "Madobi Nanami",
BuildVer: 1,
BuildVer: 2,
CommitHash: "unknown",
Description: "Remote server monitoring and command execution subsystem for Komari",
BuildType: "Debug",
BuildType: "pre-release",
BuildTime: "unknown",
}
type ConfigPathStr struct {
Global string
BotUserConfig string
BotNodeConfig string
}
var ConfigPath = ConfigPathStr{
Global: "config.json",
BotUserConfig: "bot_user_config.json",
BotNodeConfig: "bot_node_config.json",
}
+9 -9
View File
@@ -165,26 +165,26 @@ func (m *Manager) ShowBotServerList() {
}
}
// NotifyStatusChange sends a status change notification to all enabled controllers.
// NotifyStatusChange sends a status change notification to all enabled
// controllers. It honors the per-node allow-list in bot_node_config.json: a
// node whose enableStatusNotify is not true is skipped entirely, so no
// controller (chat bots or notification pipes) broadcasts its change.
func (m *Manager) NotifyStatusChange(change node.StatusChange) {
if !config.NodeStatusNotifyEnabled(change.UUID) {
postLog.Debug(fmt.Sprintf("Status change for node %s skipped: enableStatusNotify is not enabled", change.UUID))
return
}
m.mu.RLock()
defer m.mu.RUnlock()
cfg := config.C_globalConfig
templateStr := cfg.ControllerMessage.ServerStatusChanged
params := template.BuildParamsFromStatusChange(change)
for _, ctrl := range m.controllers {
if !ctrl.IsEnabled() {
continue
}
// Get the names of controllers that support commands for the message.
if err := ctrl.SendStatusChange(change); err != nil {
postLog.Warning(fmt.Sprintf("Controller %s failed to send status change: %v", ctrl.Name(), err))
}
_ = templateStr
_ = params
}
}
+15
View File
@@ -13,6 +13,7 @@ import (
"time"
"nukumizu-backend/config"
"nukumizu-backend/global"
"nukumizu-backend/internal/node"
"nukumizu-backend/postLog"
)
@@ -225,6 +226,20 @@ func (c *Client) FetchNodes() ([]NodeInfo, error) {
nodes = append(nodes, n)
}
// Persist the fetched node UUIDs to bot_node_config.json on every fetch
// (startup login, WebSocket reconnect and periodic refresh all funnel
// through FetchNodes) so the file always reflects the nodes Komari
// currently manages. A persistence failure is only logged: the node data
// itself was fetched successfully and must not be discarded over a disk
// write problem.
uuids := make([]string, len(nodes))
for i, n := range nodes {
uuids[i] = n.UUID
}
if err := config.SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, uuids); err != nil {
postLog.Warning("Failed to save bot node config: " + err.Error())
}
postLog.Info(fmt.Sprintf("Fetched %d nodes from Komari", len(nodes)))
return nodes, nil
}
+2 -8
View File
@@ -1,7 +1,6 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
@@ -34,21 +33,16 @@ func main() {
global.SoftwareInfo.CommitHash = CommitHash
global.SoftwareInfo.BuildTime = BuildTime
// Parse CLI flags.
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.LoadGlobalConfig(*configPath_global)
cfg, err := config.LoadGlobalConfig(global.ConfigPath.Global)
if err != nil {
log.Fatalf("Failed to load global config: %v", err)
}
_, err = config.LoadBotUserConfig(*configPath_bot_user)
_, err = config.LoadBotUserConfig(global.ConfigPath.BotUserConfig)
if err != nil {
log.Fatalf("Failed to load bot user config: %v", err)
}