Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac5587d0c8 | ||
|
|
2524e40566 | ||
|
|
5167d799bc | ||
|
|
cb2df5076d | ||
|
|
2d5cf39151 | ||
|
|
05dcc3769d | ||
|
|
f36f17bd2a | ||
|
|
3877c6d128 | ||
|
|
3ce42b3cca | ||
|
|
057d7d584d | ||
|
|
90ca08066b | ||
|
|
d481f13d1d | ||
|
|
7ecf51eba5 | ||
|
|
59c0f5d5d8 | ||
|
|
16455027ec | ||
|
|
4be6d2add0 | ||
|
|
15144fb9d4 | ||
|
|
a89ad8143a | ||
|
|
fb5b46a548 | ||
|
|
f6092cd178 | ||
|
|
3cba5df5c7 | ||
|
|
b2566d45f8 | ||
|
|
8b88377d4f | ||
|
|
f6af57cf58 | ||
|
|
160918a93e | ||
|
|
f622baadbd | ||
|
|
78284ed816 | ||
|
|
d96b90b5bf | ||
|
|
c0eada9bcc | ||
|
|
378727ac57 | ||
|
|
8b43e8b2ea | ||
|
|
786f364743 | ||
|
|
a0e2df615c |
@@ -6,6 +6,10 @@ bot_node_config.json
|
|||||||
*.exe
|
*.exe
|
||||||
nukumizu-linux-amd64
|
nukumizu-linux-amd64
|
||||||
|
|
||||||
|
# Scratch test files dropped in the repo root stay untracked; the real suite
|
||||||
|
# lives next to the code it covers (config/, handler/, ...) and is tracked.
|
||||||
|
/*test.go
|
||||||
|
|
||||||
# The built web console. web/embed.go compiles it into the binary, and the
|
# The built web console. web/embed.go compiles it into the binary, and the
|
||||||
# build-*.sh / build-*.bat scripts rebuild it before every compile.
|
# build-*.sh / build-*.bat scripts rebuild it before every compile.
|
||||||
/web/dist/
|
/web/dist/
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"nukumizu-backend/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestConcurrentReloadAndRead drives every configuration accessor from reader
|
||||||
|
// goroutines while UpdateSettings and SaveBotNodeConfig replace the in-memory
|
||||||
|
// configurations underneath them. Run with -race to check that the swap is
|
||||||
|
// race-free: before the singletons were published through atomic pointers this
|
||||||
|
// pattern was an unsynchronized read of a variable written by LoadGlobalConfig
|
||||||
|
// and friends, which the race detector reports.
|
||||||
|
//
|
||||||
|
// The readers call the accessors the way production code does — take the value
|
||||||
|
// and use it immediately, never store it — because that is what keeps a reader
|
||||||
|
// pinned to one complete version of the configuration.
|
||||||
|
func TestConcurrentReloadAndRead(t *testing.T) {
|
||||||
|
writeTempConfig(t, &global.ConfigPath.Global, `{
|
||||||
|
"system": {
|
||||||
|
"debugMode": true,
|
||||||
|
"listenPort": "8080"
|
||||||
|
},
|
||||||
|
"webhook": {
|
||||||
|
"endpoints": {
|
||||||
|
"example": { "enabled": true, "token": "t", "notifyPipes": ["ntfy"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"controllerMessage": {
|
||||||
|
"BOT_STARTED": "hello"
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
|
||||||
|
"qq(napcat)": {
|
||||||
|
"admins": { "1": { "event_reply": true } },
|
||||||
|
"trustedGroups": { "2": { "event_status_notify": true } }
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
writeTempConfig(t, &global.ConfigPath.BotNodeConfig, `{
|
||||||
|
"node-1": { "enableStatusNotify": true }
|
||||||
|
}`)
|
||||||
|
|
||||||
|
// Seed every singleton so the readers start from a loaded configuration
|
||||||
|
// rather than racing the first store.
|
||||||
|
if _, err := LoadGlobalConfig(global.ConfigPath.Global); err != nil {
|
||||||
|
t.Fatalf("LoadGlobalConfig: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := LoadBotUserConfig(global.ConfigPath.BotUserConfig); err != nil {
|
||||||
|
t.Fatalf("LoadBotUserConfig: %v", err)
|
||||||
|
}
|
||||||
|
if err := LoadBotNodeConfig(global.ConfigPath.BotNodeConfig); err != nil {
|
||||||
|
t.Fatalf("LoadBotNodeConfig: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readers = 4
|
||||||
|
const rounds = 40
|
||||||
|
|
||||||
|
var readersWg, writersWg sync.WaitGroup
|
||||||
|
stop := make(chan struct{})
|
||||||
|
|
||||||
|
for i := 0; i < readers; i++ {
|
||||||
|
readersWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer readersWg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if cfg := Current(); cfg != nil {
|
||||||
|
_ = cfg.System.DebugMode
|
||||||
|
_ = cfg.System.ListenPort
|
||||||
|
_ = cfg.ControllerMessage.BotStarted
|
||||||
|
_ = cfg.Webhook.Endpoints
|
||||||
|
}
|
||||||
|
if users := BotUsers(); users != nil {
|
||||||
|
_ = users.QQ.Admins.IDs()
|
||||||
|
_ = users.QQ.TrustedGroups.IDs()
|
||||||
|
}
|
||||||
|
_ = BotNodes()
|
||||||
|
_ = IsDebugMode()
|
||||||
|
_ = NodeStatusNotifyEnabled("node-1")
|
||||||
|
_ = WebhookEndpoints()
|
||||||
|
_, _ = GetWebhookEndpoint("example")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer: reloads the global and bot user configurations, and rewrites the
|
||||||
|
// node registry through UpdateSettings so its reload runs too.
|
||||||
|
writersWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer writersWg.Done()
|
||||||
|
for i := 0; i < rounds; i++ {
|
||||||
|
enabled := i%2 == 0
|
||||||
|
patch := map[string]interface{}{
|
||||||
|
"system": map[string]interface{}{"debugMode": enabled},
|
||||||
|
"controllerMessage": map[string]interface{}{
|
||||||
|
"BOT_STARTED": "hello",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := UpdateSettings(SettingGlobal, patch); err != nil {
|
||||||
|
t.Errorf("UpdateSettings(global): %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := UpdateSettings(SettingBotUserConfig, map[string]interface{}{
|
||||||
|
"qq(napcat)": map[string]interface{}{
|
||||||
|
"admins": map[string]interface{}{
|
||||||
|
"1": map[string]interface{}{"event_reply": enabled},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("UpdateSettings(bot_user_config): %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := UpdateSettings(SettingBotNodeConfig, map[string]interface{}{
|
||||||
|
"node-2": map[string]interface{}{"enableStatusNotify": enabled},
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("UpdateSettings(bot_node_config): %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Second writer: the node tracker's background save, which shares the same
|
||||||
|
// read-modify-write lock as the admin edits above.
|
||||||
|
writersWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer writersWg.Done()
|
||||||
|
for i := 0; i < rounds; i++ {
|
||||||
|
if err := SaveBotNodeConfig(global.ConfigPath.BotNodeConfig, []string{"node-1", "node-2"}); err != nil {
|
||||||
|
t.Errorf("SaveBotNodeConfig: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Let the writers finish, then release the readers. Waiting on the readers
|
||||||
|
// first would deadlock: they only return once stop is closed.
|
||||||
|
writersWg.Wait()
|
||||||
|
close(stop)
|
||||||
|
readersWg.Wait()
|
||||||
|
|
||||||
|
// The last write must be visible: the accessors are not allowed to serve a
|
||||||
|
// stale configuration once UpdateSettings has returned.
|
||||||
|
if err := UpdateSettings(SettingGlobal, map[string]interface{}{
|
||||||
|
"system": map[string]interface{}{"debugMode": true},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("final UpdateSettings(global): %v", err)
|
||||||
|
}
|
||||||
|
cfg := Current()
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatal("Current() is nil after a successful reload")
|
||||||
|
}
|
||||||
|
if !cfg.System.DebugMode {
|
||||||
|
t.Error("Current() did not observe the reloaded debugMode")
|
||||||
|
}
|
||||||
|
if cfg.System.ListenPort != "8080" {
|
||||||
|
t.Errorf("reload dropped an untouched sibling: listenPort = %q", cfg.System.ListenPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-11
@@ -17,7 +17,7 @@ func LoadBotNodeConfig(configPath string) error {
|
|||||||
data, err := os.ReadFile(configPath)
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
C_botNodeConfig = cfg
|
botNodeConfig.Store(&cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to read bot node config file: %w", err)
|
return fmt.Errorf("failed to read bot node config file: %w", err)
|
||||||
@@ -27,19 +27,17 @@ func LoadBotNodeConfig(configPath string) error {
|
|||||||
return fmt.Errorf("failed to parse bot node config file: %w", err)
|
return fmt.Errorf("failed to parse bot node config file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
C_botNodeConfig = cfg
|
botNodeConfig.Store(&cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeStatusNotifyEnabled reports whether the node identified by uuid should
|
// NodeStatusNotifyEnabled reports whether the node identified by uuid should
|
||||||
// broadcast status-change notifications, per bot_node_config.json.
|
// broadcast status-change notifications, per bot_node_config.json.
|
||||||
// enableStatusNotify defaults to true: a node notifies unless its entry
|
// enableStatusNotify defaults to true: a node notifies unless its entry
|
||||||
// explicitly sets the flag to false.
|
// explicitly sets the flag to false. A missing configuration (nil map) yields
|
||||||
|
// the same default.
|
||||||
func NodeStatusNotifyEnabled(uuid string) bool {
|
func NodeStatusNotifyEnabled(uuid string) bool {
|
||||||
if C_botNodeConfig == nil {
|
opts, ok := BotNodes()[uuid]
|
||||||
return true
|
|
||||||
}
|
|
||||||
opts, ok := C_botNodeConfig[uuid]
|
|
||||||
if !ok || opts.EnableStatusNotify == nil {
|
if !ok || opts.EnableStatusNotify == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -146,7 +144,7 @@ func LoadGlobalConfig(configPath string) (*Config, error) {
|
|||||||
cfg.ControllerMessage.ServerExecuteResult = "Command execute result:\nServer Name: {{ serverName }}\nCommand: {{ command }}\n***Result***\n\n{{ result }}\n\n************\nTime: {{ time }}"
|
cfg.ControllerMessage.ServerExecuteResult = "Command execute result:\nServer Name: {{ serverName }}\nCommand: {{ command }}\n***Result***\n\n{{ result }}\n\n************\nTime: {{ time }}"
|
||||||
}
|
}
|
||||||
|
|
||||||
C_globalConfig = &cfg
|
globalConfig.Store(&cfg)
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +160,7 @@ func LoadBotUserConfig(configPath string) (*BotUserConfig, error) {
|
|||||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse bot user config file: %w", err)
|
return nil, fmt.Errorf("failed to parse bot user config file: %w", err)
|
||||||
}
|
}
|
||||||
C_botUserConfig = &cfg
|
botUserConfig.Store(&cfg)
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,8 +211,9 @@ func SaveBotNodeConfig(configPath string, uuids []string) error {
|
|||||||
|
|
||||||
// IsDebugMode returns whether debug mode is enabled.
|
// IsDebugMode returns whether debug mode is enabled.
|
||||||
func IsDebugMode() bool {
|
func IsDebugMode() bool {
|
||||||
if C_globalConfig == nil {
|
cfg := Current()
|
||||||
|
if cfg == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return C_globalConfig.System.DebugMode
|
return cfg.System.DebugMode
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -27,13 +27,13 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
|
|||||||
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
|
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
|
||||||
"qq(napcat)": {
|
"qq(napcat)": {
|
||||||
"admins": {
|
"admins": {
|
||||||
"3526453517": {
|
"100000001": {
|
||||||
"event_status_notify": true,
|
"event_status_notify": true,
|
||||||
"event_bot_started": true
|
"event_bot_started": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trustedGroups": {
|
"trustedGroups": {
|
||||||
"740724778": {
|
"200000002": {
|
||||||
"event_status_notify": true,
|
"event_status_notify": true,
|
||||||
"event_bot_started": false
|
"event_bot_started": false
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
|
|||||||
patch := map[string]interface{}{
|
patch := map[string]interface{}{
|
||||||
"qq(napcat)": map[string]interface{}{
|
"qq(napcat)": map[string]interface{}{
|
||||||
"admins": map[string]interface{}{
|
"admins": map[string]interface{}{
|
||||||
"3526453517": map[string]interface{}{
|
"100000001": map[string]interface{}{
|
||||||
"event_status_notify": false, // toggle an existing nested flag
|
"event_status_notify": false, // toggle an existing nested flag
|
||||||
"event_reply": true, // add a key that is not in the file
|
"event_reply": true, // add a key that is not in the file
|
||||||
},
|
},
|
||||||
@@ -70,7 +70,7 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
|
|||||||
`"event_status_notify": false`,
|
`"event_status_notify": false`,
|
||||||
`"event_reply": true`,
|
`"event_reply": true`,
|
||||||
`"event_bot_started": true`,
|
`"event_bot_started": true`,
|
||||||
`"event_status_notify": true`, // sibling under trustedGroups 740724778 kept
|
`"event_status_notify": true`, // sibling under trustedGroups 200000002 kept
|
||||||
`"12345"`,
|
`"12345"`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
@@ -79,16 +79,17 @@ func TestUpdateSettingsDeepMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The in-memory singleton must reflect the merged file too.
|
// The in-memory singleton must reflect the merged file too.
|
||||||
if C_botUserConfig == nil {
|
users := BotUsers()
|
||||||
t.Fatal("C_botUserConfig not reloaded")
|
if users == nil {
|
||||||
|
t.Fatal("bot user config not reloaded")
|
||||||
}
|
}
|
||||||
if C_botUserConfig.QQ.Admins["3526453517"].EventStatusNotify {
|
if users.QQ.Admins["100000001"].EventStatusNotify {
|
||||||
t.Error("expected reloaded admin event_status_notify = false")
|
t.Error("expected reloaded admin event_status_notify = false")
|
||||||
}
|
}
|
||||||
if !C_botUserConfig.QQ.Admins["3526453517"].EventReply {
|
if !users.QQ.Admins["100000001"].EventReply {
|
||||||
t.Error("expected reloaded admin event_reply = true")
|
t.Error("expected reloaded admin event_reply = true")
|
||||||
}
|
}
|
||||||
if !C_botUserConfig.QQ.TrustedGroups["12345"].EventBotStarted {
|
if !users.QQ.TrustedGroups["12345"].EventBotStarted {
|
||||||
t.Error("expected new trusted group event_bot_started = true")
|
t.Error("expected new trusted group event_bot_started = true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,8 +182,8 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
|
|||||||
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
|
writeTempConfig(t, &global.ConfigPath.BotUserConfig, `{
|
||||||
"qq(napcat)": {
|
"qq(napcat)": {
|
||||||
"admins": {
|
"admins": {
|
||||||
"3526453517": { "event_status_notify": true },
|
"100000001": { "event_status_notify": true },
|
||||||
"740724778": { "event_status_notify": false }
|
"200000002": { "event_status_notify": false }
|
||||||
},
|
},
|
||||||
"trustedGroups": {
|
"trustedGroups": {
|
||||||
"999": { "event_bot_started": true }
|
"999": { "event_bot_started": true }
|
||||||
@@ -197,7 +198,7 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
|
|||||||
patch := map[string]interface{}{
|
patch := map[string]interface{}{
|
||||||
"qq(napcat)": map[string]interface{}{
|
"qq(napcat)": map[string]interface{}{
|
||||||
"admins": map[string]interface{}{
|
"admins": map[string]interface{}{
|
||||||
"3526453517": nil,
|
"100000001": nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -210,10 +211,10 @@ func TestUpdateSettingsRemovesKeysWithNull(t *testing.T) {
|
|||||||
t.Fatalf("GetSettings: %v", err)
|
t.Fatalf("GetSettings: %v", err)
|
||||||
}
|
}
|
||||||
got := string(data)
|
got := string(data)
|
||||||
if strings.Contains(got, "3526453517") {
|
if strings.Contains(got, "100000001") {
|
||||||
t.Errorf("deleted member still present:\n%s", got)
|
t.Errorf("deleted member still present:\n%s", got)
|
||||||
}
|
}
|
||||||
for _, want := range []string{"740724778", `"trustedGroups"`, `"telegram"`} {
|
for _, want := range []string{"200000002", `"trustedGroups"`, `"telegram"`} {
|
||||||
if !strings.Contains(got, want) {
|
if !strings.Contains(got, want) {
|
||||||
t.Errorf("unrelated content missing %q:\n%s", want, got)
|
t.Errorf("unrelated content missing %q:\n%s", want, got)
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-8
@@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import "sort"
|
import (
|
||||||
|
"sort"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
// SystemConfig holds system-level configuration.
|
// SystemConfig holds system-level configuration.
|
||||||
type SystemConfig struct {
|
type SystemConfig struct {
|
||||||
@@ -136,10 +139,11 @@ type WebhookReceiverConfig struct {
|
|||||||
// GetWebhookEndpoint returns the incoming webhook endpoint registered under the
|
// GetWebhookEndpoint returns the incoming webhook endpoint registered under the
|
||||||
// given name, and whether such an endpoint exists.
|
// given name, and whether such an endpoint exists.
|
||||||
func GetWebhookEndpoint(name string) (WebhookEndpointConfig, bool) {
|
func GetWebhookEndpoint(name string) (WebhookEndpointConfig, bool) {
|
||||||
if C_globalConfig == nil {
|
cfg := Current()
|
||||||
|
if cfg == nil {
|
||||||
return WebhookEndpointConfig{}, false
|
return WebhookEndpointConfig{}, false
|
||||||
}
|
}
|
||||||
endpoint, ok := C_globalConfig.Webhook.Endpoints[name]
|
endpoint, ok := cfg.Webhook.Endpoints[name]
|
||||||
return endpoint, ok
|
return endpoint, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +169,19 @@ type Config struct {
|
|||||||
DBPath string `json:"dbPath"`
|
DBPath string `json:"dbPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var C_globalConfig *Config
|
// globalConfig holds the configuration currently in effect. It is replaced as a
|
||||||
|
// whole by LoadGlobalConfig — and therefore by every settings update — and never
|
||||||
|
// mutated in place, so a reader that loads the pointer always observes a fully
|
||||||
|
// initialized Config. Read it through Current rather than caching the result: a
|
||||||
|
// cached pointer stops tracking reloads.
|
||||||
|
var globalConfig atomic.Pointer[Config]
|
||||||
|
|
||||||
|
// Current returns the configuration currently in effect, or nil before the
|
||||||
|
// first successful LoadGlobalConfig. It is safe to call from any goroutine, and
|
||||||
|
// must be called on every use rather than stored, so the caller sees reloads.
|
||||||
|
func Current() *Config {
|
||||||
|
return globalConfig.Load()
|
||||||
|
}
|
||||||
|
|
||||||
// BotUserOptions holds per-member options stored in bot_user_config.json.
|
// BotUserOptions holds per-member options stored in bot_user_config.json.
|
||||||
type BotUserOptions struct {
|
type BotUserOptions struct {
|
||||||
@@ -220,7 +236,16 @@ type BotUserConfig struct {
|
|||||||
Telegram BotUser_TelegramConfig `json:"telegram"`
|
Telegram BotUser_TelegramConfig `json:"telegram"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var C_botUserConfig *BotUserConfig
|
// botUserConfig mirrors bot_user_config.json the same way globalConfig mirrors
|
||||||
|
// config.json: replaced wholesale on reload and read through BotUsers.
|
||||||
|
var botUserConfig atomic.Pointer[BotUserConfig]
|
||||||
|
|
||||||
|
// BotUsers returns the bot user configuration currently in effect, or nil
|
||||||
|
// before the first successful LoadBotUserConfig. Like Current it must be called
|
||||||
|
// on every use rather than stored.
|
||||||
|
func BotUsers() *BotUserConfig {
|
||||||
|
return botUserConfig.Load()
|
||||||
|
}
|
||||||
|
|
||||||
// BotNodeOptions holds per-node options stored in bot_node_config.json. The
|
// 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;
|
// file is auto-populated by the node tracker for every node Komari reports;
|
||||||
@@ -238,6 +263,18 @@ type BotNodeOptions struct {
|
|||||||
// options.
|
// options.
|
||||||
type BotNodeMembers map[string]BotNodeOptions
|
type BotNodeMembers map[string]BotNodeOptions
|
||||||
|
|
||||||
// C_botNodeConfig is the global singleton mirroring bot_node_config.json,
|
// botNodeConfig mirrors bot_node_config.json, populated by LoadBotNodeConfig.
|
||||||
// populated by LoadBotNodeConfig.
|
// The map is rebuilt rather than mutated on every load, so the pointer can be
|
||||||
var C_botNodeConfig BotNodeMembers
|
// swapped atomically; read it through BotNodes.
|
||||||
|
var botNodeConfig atomic.Pointer[BotNodeMembers]
|
||||||
|
|
||||||
|
// BotNodes returns the per-node options currently in effect, or nil before the
|
||||||
|
// first LoadBotNodeConfig. Like Current it must be called on every use rather
|
||||||
|
// than stored.
|
||||||
|
func BotNodes() BotNodeMembers {
|
||||||
|
nodes := botNodeConfig.Load()
|
||||||
|
if nodes == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *nodes
|
||||||
|
}
|
||||||
|
|||||||
+9
-7
@@ -37,10 +37,11 @@ var webhookEndpointFields = map[string]func(interface{}) bool{
|
|||||||
// configuration.
|
// configuration.
|
||||||
func WebhookEndpoints() map[string]WebhookEndpointConfig {
|
func WebhookEndpoints() map[string]WebhookEndpointConfig {
|
||||||
endpoints := map[string]WebhookEndpointConfig{}
|
endpoints := map[string]WebhookEndpointConfig{}
|
||||||
if C_globalConfig == nil {
|
cfg := Current()
|
||||||
|
if cfg == nil {
|
||||||
return endpoints
|
return endpoints
|
||||||
}
|
}
|
||||||
for name, endpoint := range C_globalConfig.Webhook.Endpoints {
|
for name, endpoint := range cfg.Webhook.Endpoints {
|
||||||
endpoints[name] = endpoint
|
endpoints[name] = endpoint
|
||||||
}
|
}
|
||||||
return endpoints
|
return endpoints
|
||||||
@@ -107,14 +108,15 @@ func DeleteWebhookEndpoint(name string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// webhookEndpoint returns the named endpoint held by the loaded configuration.
|
// webhookEndpoint returns the named endpoint held by the loaded configuration.
|
||||||
// No lock is needed to read it: a reload replaces the whole configuration
|
// No extra lock is needed to read it: a reload replaces the whole configuration
|
||||||
// rather than mutating it in place, and the value is read from whichever
|
// rather than mutating it in place, and Current publishes the replacement
|
||||||
// version is current.
|
// atomically, so the value read is always from one complete version.
|
||||||
func webhookEndpoint(name string) (WebhookEndpointConfig, bool) {
|
func webhookEndpoint(name string) (WebhookEndpointConfig, bool) {
|
||||||
if C_globalConfig == nil {
|
cfg := Current()
|
||||||
|
if cfg == nil {
|
||||||
return WebhookEndpointConfig{}, false
|
return WebhookEndpointConfig{}, false
|
||||||
}
|
}
|
||||||
endpoint, exists := C_globalConfig.Webhook.Endpoints[name]
|
endpoint, exists := cfg.Webhook.Endpoints[name]
|
||||||
return endpoint, exists
|
return endpoint, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-2
@@ -51,13 +51,27 @@ func setupAdminToken() {
|
|||||||
utils.AddToken("test-admin-token", 1, "admin", "tester")
|
utils.AddToken("test-admin-token", 1, "admin", "tester")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decodeResponse decodes a success envelope and returns its data object, which
|
||||||
|
// the getInfo and getStatus handlers key by uuid. Responses are wrapped by
|
||||||
|
// utils.SendSuccessResponse as {success, message?, data:{...}}, the shape the
|
||||||
|
// console reads as `response.data[uuid]` (see frontend/src/api/index.js), so
|
||||||
|
// the tests index the returned map by uuid rather than by envelope key.
|
||||||
func decodeResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]json.RawMessage {
|
func decodeResponse(t *testing.T, w *httptest.ResponseRecorder) map[string]json.RawMessage {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
var body map[string]json.RawMessage
|
var body struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data map[string]json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||||
t.Fatalf("decode response: %v; body=%s", err, w.Body.String())
|
t.Fatalf("decode response: %v; body=%s", err, w.Body.String())
|
||||||
}
|
}
|
||||||
return body
|
if !body.Success {
|
||||||
|
t.Fatalf("response is not a success envelope: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
if body.Data == nil {
|
||||||
|
t.Fatalf("response has no data object: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
return body.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServerGetInfoAll(t *testing.T) {
|
func TestServerGetInfoAll(t *testing.T) {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (m *Manager) ShowBotInitMessage() {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildBotInitializationMsgParams()
|
params := template.BuildBotInitializationMsgParams()
|
||||||
|
|
||||||
for _, ctrl := range m.controllers {
|
for _, ctrl := range m.controllers {
|
||||||
@@ -176,7 +176,7 @@ func (m *Manager) ShowBotServerList() {
|
|||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
|
|
||||||
for _, ctrl := range m.controllers {
|
for _, ctrl := range m.controllers {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func (e *EmailController) SendStatusChange(change node.StatusChange) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
params := template.BuildParamsFromStatusChange(change)
|
||||||
body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, e.cfg.Markdown)
|
body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, e.cfg.Markdown)
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ func (e *EmailController) SendServerList(onlineServers, offlineServers string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
body := template.Render(cfg.ControllerMessage.ServerList, params, e.cfg.Markdown)
|
body := template.Render(cfg.ControllerMessage.ServerList, params, e.cfg.Markdown)
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ func (e *EmailController) SendExecuteResult(serverName, serverUUID, command, res
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||||
body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, e.cfg.Markdown)
|
body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, e.cfg.Markdown)
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (n *NtfyController) SendStatusChange(change node.StatusChange) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
params := template.BuildParamsFromStatusChange(change)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, n.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, n.cfg.Markdown)
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func (n *NtfyController) SendServerList(onlineServers, offlineServers string) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
message := template.Render(cfg.ControllerMessage.ServerList, params, n.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerList, params, n.cfg.Markdown)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ func (n *NtfyController) SendExecuteResult(serverName, serverUUID, command, resu
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, n.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, n.cfg.Markdown)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ import (
|
|||||||
"nukumizu-backend/postLog"
|
"nukumizu-backend/postLog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// actionLogEnabled reports whether the NapCat HTTP API methods echo each action
|
||||||
|
// they send, per the showNapcatAction toggle. Unlike the WebSocket message path
|
||||||
|
// in qq.go it deliberately does not also require debugMode: these methods have
|
||||||
|
// always logged on this toggle alone, and preserving that is intentional.
|
||||||
|
func actionLogEnabled() bool {
|
||||||
|
cfg := config.Current()
|
||||||
|
return cfg != nil && cfg.Debug.ShowNapcatAction
|
||||||
|
}
|
||||||
|
|
||||||
// APIResponse mirrors NapCat's HTTP API response envelope.
|
// APIResponse mirrors NapCat's HTTP API response envelope.
|
||||||
type APIResponse struct {
|
type APIResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -155,7 +164,7 @@ func (c *Client) SendMsg(targetType string, targetID int64, msg string, hasAt bo
|
|||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.C_globalConfig.Debug.ShowNapcatAction {
|
if actionLogEnabled() {
|
||||||
postLog.Debug(fmt.Sprintf("[Napcat] SendMsg -> %s (%s): %s", endpoint, targetType, message))
|
postLog.Debug(fmt.Sprintf("[Napcat] SendMsg -> %s (%s): %s", endpoint, targetType, message))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +185,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) {
|
|||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.C_globalConfig.Debug.ShowNapcatAction {
|
if actionLogEnabled() {
|
||||||
postLog.Debug(fmt.Sprintf("[Napcat] RecallMsg -> /delete_msg: %d", msgID))
|
postLog.Debug(fmt.Sprintf("[Napcat] RecallMsg -> /delete_msg: %d", msgID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +199,7 @@ func (c *Client) RecallMsg(msgID int64) (*APIResponse, error) {
|
|||||||
|
|
||||||
// GetGroupList retrieves the list of joined groups from NapCat.
|
// GetGroupList retrieves the list of joined groups from NapCat.
|
||||||
func (c *Client) GetGroupList() (*APIResponse, error) {
|
func (c *Client) GetGroupList() (*APIResponse, error) {
|
||||||
if config.C_globalConfig.Debug.ShowNapcatAction {
|
if actionLogEnabled() {
|
||||||
postLog.Debug("[Napcat] GetGroupList -> /get_group_list")
|
postLog.Debug("[Napcat] GetGroupList -> /get_group_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +220,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) {
|
|||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.C_globalConfig.Debug.ShowNapcatAction {
|
if actionLogEnabled() {
|
||||||
postLog.Debug(fmt.Sprintf("[Napcat] GetGroupInfo -> /get_group_info: %d", groupID))
|
postLog.Debug(fmt.Sprintf("[Napcat] GetGroupInfo -> /get_group_info: %d", groupID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +234,7 @@ func (c *Client) GetGroupInfo(groupID int64) (*APIResponse, error) {
|
|||||||
|
|
||||||
// GetFriendsList retrieves the friends list from NapCat.
|
// GetFriendsList retrieves the friends list from NapCat.
|
||||||
func (c *Client) GetFriendsList() (*APIResponse, error) {
|
func (c *Client) GetFriendsList() (*APIResponse, error) {
|
||||||
if config.C_globalConfig.Debug.ShowNapcatAction {
|
if actionLogEnabled() {
|
||||||
postLog.Debug("[Napcat] GetFriendsList -> /get_friend_list")
|
postLog.Debug("[Napcat] GetFriendsList -> /get_friend_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,24 +100,34 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatMsg {
|
// The configuration is read once per event: the debug guards below are
|
||||||
|
// evaluated several times, and taking them all from one version keeps a
|
||||||
|
// concurrent reload from splitting them mid-event.
|
||||||
|
cfg := config.Current()
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
debugMode := cfg.System.DebugMode
|
||||||
|
showAction := debugMode && cfg.Debug.ShowNapcatAction
|
||||||
|
|
||||||
|
if debugMode && cfg.Debug.ShowNapcatMsg {
|
||||||
postLog.Debug("Napcat WS event received: " + string(raw))
|
postLog.Debug("Napcat WS event received: " + string(raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only handle message events; ignore notice/request/meta_event.
|
// Only handle message events; ignore notice/request/meta_event.
|
||||||
if ev.PostType != "message" {
|
if ev.PostType != "message" {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
|
if showAction {
|
||||||
postLog.Debug("Ignoring Napcat WS event: " + string(raw))
|
postLog.Debug("Ignoring Napcat WS event: " + string(raw))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore messages the bot itself sent (echo prevention).
|
// Ignore messages the bot itself sent (echo prevention).
|
||||||
if q.isSelfMessage(ev) && !config.C_globalConfig.System.DebugMode {
|
if q.isSelfMessage(ev) && !debugMode {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if q.isSelfMessage(ev) && config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.NapcatIgnoreSelfMsg {
|
if q.isSelfMessage(ev) && debugMode && cfg.Debug.NapcatIgnoreSelfMsg {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
|
if showAction {
|
||||||
postLog.Debug("Ignoring Napcat WS self message: " + string(raw))
|
postLog.Debug("Ignoring Napcat WS self message: " + string(raw))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -138,7 +148,7 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
|
|||||||
|
|
||||||
response := q.processCommand(cmd)
|
response := q.processCommand(cmd)
|
||||||
if response == "" {
|
if response == "" {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
|
if showAction {
|
||||||
postLog.Debug("Napcat WS command discarded: " + string(raw))
|
postLog.Debug("Napcat WS command discarded: " + string(raw))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -160,6 +170,7 @@ func (q *QQController) handleNapcatEvent(raw []byte) {
|
|||||||
// complete command to the unified processor. It returns the response text to
|
// complete command to the unified processor. It returns the response text to
|
||||||
// reply with; an empty response means the message was discarded.
|
// reply with; an empty response means the message was discarded.
|
||||||
func (q *QQController) processCommand(cmd controller.Command) string {
|
func (q *QQController) processCommand(cmd controller.Command) string {
|
||||||
|
cfg := config.Current()
|
||||||
text := cmd.RawText
|
text := cmd.RawText
|
||||||
|
|
||||||
// In "at" listen mode, require an @mention of the bot and strip it before
|
// In "at" listen mode, require an @mention of the bot and strip it before
|
||||||
@@ -168,7 +179,7 @@ func (q *QQController) processCommand(cmd controller.Command) string {
|
|||||||
if q.cfg.ListenMethod == "at" {
|
if q.cfg.ListenMethod == "at" {
|
||||||
atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID)
|
atMention := fmt.Sprintf("[CQ:at,qq=%d]", q.cfg.BotQQID)
|
||||||
if !strings.Contains(text, atMention) {
|
if !strings.Contains(text, atMention) {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowNapcatAction {
|
if cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowNapcatAction {
|
||||||
postLog.Debug("Napcat WS message ignored (no @mention): " + text)
|
postLog.Debug("Napcat WS message ignored (no @mention): " + text)
|
||||||
}
|
}
|
||||||
return "" // Not mentioned, ignore.
|
return "" // Not mentioned, ignore.
|
||||||
@@ -190,7 +201,7 @@ func (q *QQController) processCommand(cmd controller.Command) string {
|
|||||||
// Hand the complete command to the unified processor, which checks group
|
// Hand the complete command to the unified processor, which checks group
|
||||||
// vs private, trusted groups, admin permissions, and executes it.
|
// vs private, trusted groups, admin permissions, and executes it.
|
||||||
response, err := controller.GetManager().Trigger(parsed, q.trustedGroupIDs(), q.adminIDs(), q.cfg.ListenMethod)
|
response, err := controller.GetManager().Trigger(parsed, q.trustedGroupIDs(), q.adminIDs(), q.cfg.ListenMethod)
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTriggerCmdEcho {
|
if cfg != nil && cfg.System.DebugMode && cfg.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))
|
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 {
|
if err != nil {
|
||||||
@@ -213,7 +224,7 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool {
|
|||||||
|
|
||||||
// adminIDs returns the QQ admin IDs from bot_user_config.json.
|
// adminIDs returns the QQ admin IDs from bot_user_config.json.
|
||||||
func (q *QQController) adminIDs() []string {
|
func (q *QQController) adminIDs() []string {
|
||||||
if c := config.C_botUserConfig; c != nil {
|
if c := config.BotUsers(); c != nil {
|
||||||
return c.QQ.Admins.IDs()
|
return c.QQ.Admins.IDs()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -221,7 +232,7 @@ func (q *QQController) adminIDs() []string {
|
|||||||
|
|
||||||
// trustedGroupIDs returns the QQ trusted group IDs from bot_user_config.json.
|
// trustedGroupIDs returns the QQ trusted group IDs from bot_user_config.json.
|
||||||
func (q *QQController) trustedGroupIDs() []string {
|
func (q *QQController) trustedGroupIDs() []string {
|
||||||
if c := config.C_botUserConfig; c != nil {
|
if c := config.BotUsers(); c != nil {
|
||||||
return c.QQ.TrustedGroups.IDs()
|
return c.QQ.TrustedGroups.IDs()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -237,7 +248,7 @@ func (q *QQController) SendMessage(message controller.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only notify trusted groups and admins whose options allow this message type.
|
// Only notify trusted groups and admins whose options allow this message type.
|
||||||
if uc := config.C_botUserConfig; uc != nil {
|
if uc := config.BotUsers(); uc != nil {
|
||||||
for groupID, opts := range uc.QQ.TrustedGroups {
|
for groupID, opts := range uc.QQ.TrustedGroups {
|
||||||
if !controller.MemberReceives(opts, message.Type) {
|
if !controller.MemberReceives(opts, message.Type) {
|
||||||
continue
|
continue
|
||||||
@@ -260,12 +271,12 @@ func (q *QQController) SendStatusChange(change node.StatusChange) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
params := template.BuildParamsFromStatusChange(change)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, q.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, q.cfg.Markdown)
|
||||||
|
|
||||||
// Only notify trusted groups and admins whose event_status_notify is true.
|
// Only notify trusted groups and admins whose event_status_notify is true.
|
||||||
if uc := config.C_botUserConfig; uc != nil {
|
if uc := config.BotUsers(); uc != nil {
|
||||||
for groupID, opts := range uc.QQ.TrustedGroups {
|
for groupID, opts := range uc.QQ.TrustedGroups {
|
||||||
if !opts.EventStatusNotify {
|
if !opts.EventStatusNotify {
|
||||||
continue
|
continue
|
||||||
@@ -289,7 +300,7 @@ func (q *QQController) SendServerList(onlineServers, offlineServers string) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
message := template.Render(cfg.ControllerMessage.ServerList, params, q.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerList, params, q.cfg.Markdown)
|
||||||
|
|
||||||
@@ -305,7 +316,7 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, q.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, q.cfg.Markdown)
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (t *TelegramController) handleUpdate(_ context.Context, _ *bot.Bot, update
|
|||||||
}
|
}
|
||||||
msg := update.Message
|
msg := update.Message
|
||||||
|
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTelegramMsg {
|
if cfg := config.Current(); cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowTelegramMsg {
|
||||||
raw, _ := json.Marshal(update)
|
raw, _ := json.Marshal(update)
|
||||||
postLog.Debug("Telegram update received: " + string(raw))
|
postLog.Debug("Telegram update received: " + string(raw))
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ func (t *TelegramController) processCommand(cmd controller.Command) string {
|
|||||||
// Hand the complete command to the unified processor, which checks group
|
// Hand the complete command to the unified processor, which checks group
|
||||||
// vs private, trusted groups, admin permissions, and executes it.
|
// vs private, trusted groups, admin permissions, and executes it.
|
||||||
response, err := controller.GetManager().Trigger(parsed, t.trustedGroupIDs(), t.resolvedAdminList(), t.cfg.ListenMethod)
|
response, err := controller.GetManager().Trigger(parsed, t.trustedGroupIDs(), t.resolvedAdminList(), t.cfg.ListenMethod)
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowTriggerCmdEcho {
|
if cfg := config.Current(); cfg != nil && cfg.System.DebugMode && cfg.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))
|
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 {
|
if err != nil {
|
||||||
@@ -252,7 +252,7 @@ func (t *TelegramController) SendMessage(message controller.Message) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only notify trusted groups and admins whose options allow this message type.
|
// Only notify trusted groups and admins whose options allow this message type.
|
||||||
if uc := config.C_botUserConfig; uc != nil {
|
if uc := config.BotUsers(); uc != nil {
|
||||||
for groupID, opts := range uc.Telegram.TrustedGroups {
|
for groupID, opts := range uc.Telegram.TrustedGroups {
|
||||||
if !controller.MemberReceives(opts, message.Type) {
|
if !controller.MemberReceives(opts, message.Type) {
|
||||||
continue
|
continue
|
||||||
@@ -275,12 +275,12 @@ func (t *TelegramController) SendStatusChange(change node.StatusChange) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
params := template.BuildParamsFromStatusChange(change)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, t.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, t.cfg.Markdown)
|
||||||
|
|
||||||
// Only notify trusted groups and admins whose event_status_notify is true.
|
// Only notify trusted groups and admins whose event_status_notify is true.
|
||||||
if uc := config.C_botUserConfig; uc != nil {
|
if uc := config.BotUsers(); uc != nil {
|
||||||
for groupID, opts := range uc.Telegram.TrustedGroups {
|
for groupID, opts := range uc.Telegram.TrustedGroups {
|
||||||
if !opts.EventStatusNotify {
|
if !opts.EventStatusNotify {
|
||||||
continue
|
continue
|
||||||
@@ -303,7 +303,7 @@ func (t *TelegramController) SendServerList(onlineServers, offlineServers string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
message := template.Render(cfg.ControllerMessage.ServerList, params, t.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerList, params, t.cfg.Markdown)
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ func (t *TelegramController) SendExecuteResult(serverName, serverUUID, command,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, t.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, t.cfg.Markdown)
|
||||||
|
|
||||||
@@ -395,7 +395,7 @@ func (t *TelegramController) resolveUsername(username string) (int64, bool) {
|
|||||||
// adminIDs returns the Telegram admin entries (numeric user ID or @username)
|
// adminIDs returns the Telegram admin entries (numeric user ID or @username)
|
||||||
// from bot_user_config.json.
|
// from bot_user_config.json.
|
||||||
func (t *TelegramController) adminIDs() []string {
|
func (t *TelegramController) adminIDs() []string {
|
||||||
if c := config.C_botUserConfig; c != nil {
|
if c := config.BotUsers(); c != nil {
|
||||||
return c.Telegram.Admins.IDs()
|
return c.Telegram.Admins.IDs()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -403,7 +403,7 @@ func (t *TelegramController) adminIDs() []string {
|
|||||||
|
|
||||||
// trustedGroupIDs returns the Telegram trusted group IDs from bot_user_config.json.
|
// trustedGroupIDs returns the Telegram trusted group IDs from bot_user_config.json.
|
||||||
func (t *TelegramController) trustedGroupIDs() []string {
|
func (t *TelegramController) trustedGroupIDs() []string {
|
||||||
if c := config.C_botUserConfig; c != nil {
|
if c := config.BotUsers(); c != nil {
|
||||||
return c.Telegram.TrustedGroups.IDs()
|
return c.Telegram.TrustedGroups.IDs()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (w *WebhookController) SendStatusChange(change node.StatusChange) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromStatusChange(change)
|
params := template.BuildParamsFromStatusChange(change)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, w.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, w.cfg.Markdown)
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func (w *WebhookController) SendServerList(onlineServers, offlineServers string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
message := template.Render(cfg.ControllerMessage.ServerList, params, w.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerList, params, w.cfg.Markdown)
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func (w *WebhookController) SendExecuteResult(serverName, serverUUID, command, r
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, w.cfg.Markdown)
|
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, w.cfg.Markdown)
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ func commandMarkdown(cmd Command) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleHelp(cmd Command) (string, error) {
|
func handleHelp(cmd Command) (string, error) {
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildBotInitializationMsgParams()
|
params := template.BuildBotInitializationMsgParams()
|
||||||
return template.Render(cfg.ControllerMessage.BotHelp, params, commandMarkdown(cmd)), nil
|
return template.Render(cfg.ControllerMessage.BotHelp, params, commandMarkdown(cmd)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleList(cmd Command) (string, error) {
|
func handleList(cmd Command) (string, error) {
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromServerList()
|
params := template.BuildParamsFromServerList()
|
||||||
return template.Render(cfg.ControllerMessage.ServerList, params, commandMarkdown(cmd)), nil
|
return template.Render(cfg.ControllerMessage.ServerList, params, commandMarkdown(cmd)), nil
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ func handleRun(cmd Command) (string, error) {
|
|||||||
return fmt.Sprintf("Error getting results: %v", err), nil
|
return fmt.Sprintf("Error getting results: %v", err), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results))
|
params := template.BuildParamsFromExecResult(uuidArg, uuidArg, command, formatTaskResults(results))
|
||||||
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params, commandMarkdown(cmd)), nil
|
return template.Render(cfg.ControllerMessage.ServerExecuteResult, params, commandMarkdown(cmd)), nil
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func handleInfo(cmd Command) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func telegram_handleStart(cmd Command) (string, error) {
|
func telegram_handleStart(cmd Command) (string, error) {
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
params := template.BuildBotInitializationMsgParams()
|
params := template.BuildBotInitializationMsgParams()
|
||||||
return template.Render(cfg.ControllerMessage.Tg_BotStart, params, commandMarkdown(cmd)), nil
|
return template.Render(cfg.ControllerMessage.Tg_BotStart, params, commandMarkdown(cmd)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ import (
|
|||||||
"nukumizu-backend/postLog"
|
"nukumizu-backend/postLog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// taskEchoEnabled reports whether Komari task progress should be echoed to the
|
||||||
|
// log: debug mode plus the showKomariTaskEcho toggle. The configuration is read
|
||||||
|
// once per call so both flags come from the same reload.
|
||||||
|
func taskEchoEnabled() bool {
|
||||||
|
cfg := config.Current()
|
||||||
|
return cfg != nil && cfg.System.DebugMode && cfg.Debug.ShowKomariTaskEcho
|
||||||
|
}
|
||||||
|
|
||||||
// NodeInfo represents a single node as returned by Komari's
|
// NodeInfo represents a single node as returned by Komari's
|
||||||
// common:getNodes RPC2 method.
|
// common:getNodes RPC2 method.
|
||||||
type NodeInfo struct {
|
type NodeInfo struct {
|
||||||
@@ -146,7 +154,7 @@ func (c *Client) Login(username, password string) error {
|
|||||||
|
|
||||||
var kr KomariResponse
|
var kr KomariResponse
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&kr); err != nil {
|
if err := json.NewDecoder(resp.Body).Decode(&kr); err != nil {
|
||||||
if config.C_globalConfig.System.DebugMode {
|
if config.IsDebugMode() {
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, respBody)
|
return fmt.Errorf("failed to parse komari login response: %w.\nResponse: %s", err, respBody)
|
||||||
}
|
}
|
||||||
@@ -332,7 +340,7 @@ func (c *Client) ExecTask(uuids []string, command string) (string, error) {
|
|||||||
return "", fmt.Errorf("failed to parse komari task exec data: %w", err)
|
return "", fmt.Errorf("failed to parse komari task exec data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
|
if taskEchoEnabled() {
|
||||||
postLog.Debug(fmt.Sprintf("Created Komari task %s for %d clients", result.TaskID, len(uuids)))
|
postLog.Debug(fmt.Sprintf("Created Komari task %s for %d clients", result.TaskID, len(uuids)))
|
||||||
}
|
}
|
||||||
return result.TaskID, nil
|
return result.TaskID, nil
|
||||||
@@ -375,7 +383,7 @@ func (c *Client) GetTaskResult(taskID string) ([]TaskResult, bool, error) {
|
|||||||
// PollTaskResult polls for task results every 1 second until all results are
|
// PollTaskResult polls for task results every 1 second until all results are
|
||||||
// available or 60 seconds have elapsed.
|
// available or 60 seconds have elapsed.
|
||||||
func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) {
|
func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
|
if taskEchoEnabled() {
|
||||||
postLog.Debug(fmt.Sprintf("Polling for Komari task %s results...", taskID))
|
postLog.Debug(fmt.Sprintf("Polling for Komari task %s results...", taskID))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +401,7 @@ func (c *Client) PollTaskResult(taskID string) ([]TaskResult, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if done {
|
if done {
|
||||||
if config.C_globalConfig.System.DebugMode && config.C_globalConfig.Debug.ShowKomariTaskEcho {
|
if taskEchoEnabled() {
|
||||||
postLog.Info(fmt.Sprintf("Task %s completed with %d results", taskID, len(results)))
|
postLog.Info(fmt.Sprintf("Task %s completed with %d results", taskID, len(results)))
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
|
|||||||
@@ -409,7 +409,10 @@ func GetWSClient() *WSClient {
|
|||||||
|
|
||||||
// LoginAndStart performs the Komari login and returns an error if it fails.
|
// LoginAndStart performs the Komari login and returns an error if it fails.
|
||||||
func LoginAndStart() error {
|
func LoginAndStart() error {
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("configuration not loaded")
|
||||||
|
}
|
||||||
client := GetClient()
|
client := GetClient()
|
||||||
if client == nil {
|
if client == nil {
|
||||||
return fmt.Errorf("komari client not initialized")
|
return fmt.Errorf("komari client not initialized")
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ import (
|
|||||||
// proxyURL returns the system-wide network proxy URL, or nil when none is
|
// proxyURL returns the system-wide network proxy URL, or nil when none is
|
||||||
// configured. A missing scheme is normalized to http:// for convenience.
|
// configured. A missing scheme is normalized to http:// for convenience.
|
||||||
func proxyURL() *url.URL {
|
func proxyURL() *url.URL {
|
||||||
raw := config.C_globalConfig.System.NetworkProxy
|
cfg := config.Current()
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
raw := cfg.System.NetworkProxy
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func startWebhookServer(cfg *config.Config) {
|
|||||||
|
|
||||||
// initControllers initializes and starts all configured controllers.
|
// initControllers initializes and starts all configured controllers.
|
||||||
func initControllers() {
|
func initControllers() {
|
||||||
cfg := config.C_globalConfig
|
cfg := config.Current()
|
||||||
mgr := controller.GetManager()
|
mgr := controller.GetManager()
|
||||||
if mgr == nil {
|
if mgr == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user