feat(controller): add init message for bot pipes
chore: move softwareInfo from main package to a individual global package
This commit is contained in:
@@ -88,6 +88,9 @@ func LoadConfig(configPath string) (*Config, error) {
|
||||
}
|
||||
|
||||
// Apply default message templates if not specified.
|
||||
if cfg.ControllerMessage.BotStarted == "" {
|
||||
cfg.ControllerMessage.BotStarted = "Nukumizu Alert Bot Started\nVersion: {{ softwareVersion }}.{{ softwareBuildVer }}.{{ softwareCommitHash }}.{{ softwareBuildType }}\nDeveloper: {{ softwareDeveloper }}\nTime: {{ time }}"
|
||||
}
|
||||
if cfg.ControllerMessage.ServerStatusChanged == "" {
|
||||
cfg.ControllerMessage.ServerStatusChanged = "Server Status Changed Alert\n{{ serverName }} - {{ upStatus }}\nEvent: {{ event }}\nServer Name: {{ serverName }}\nMessage: {{ message }}\nTime: {{ time }}"
|
||||
}
|
||||
|
||||
+5
-4
@@ -9,10 +9,10 @@ type SystemConfig struct {
|
||||
|
||||
// DebugConfig holds debug-level configuration.
|
||||
type DebugConfig struct {
|
||||
ShowNapcatMsg bool `json:"showNapcatMsg"`
|
||||
ShowNapcatAction bool `json:"showNapcatAction"`
|
||||
ShowTelegramMsg bool `json:"showTelegramMsg"`
|
||||
ShowTriggerCmdEcho bool `json:"showTriggerCmdEcho"`
|
||||
ShowNapcatMsg bool `json:"showNapcatMsg"`
|
||||
ShowNapcatAction bool `json:"showNapcatAction"`
|
||||
ShowTelegramMsg bool `json:"showTelegramMsg"`
|
||||
ShowTriggerCmdEcho bool `json:"showTriggerCmdEcho"`
|
||||
NapcatIgnoreSelfMsg bool `json:"napcatIgnoreSelfMsg"`
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ type ControllerMethodConfig struct {
|
||||
|
||||
// ControllerMessageConfig holds message templates for controller responses.
|
||||
type ControllerMessageConfig struct {
|
||||
BotStarted string `json:"BOT_STARTED"`
|
||||
ServerStatusChanged string `json:"SERVER_STATUS_CHANGED"`
|
||||
ServerList string `json:"SERVER_LIST"`
|
||||
ServerExecuteResult string `json:"SERVER_EXECUTE_RESULT"`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package global
|
||||
|
||||
// SoftwareInfo holds build metadata.
|
||||
type SoftwareInfoStr struct {
|
||||
Name string
|
||||
Version string
|
||||
Developer string
|
||||
BuildVer int16
|
||||
CommitHash string
|
||||
Description string
|
||||
BuildType string
|
||||
BuildTime string
|
||||
}
|
||||
|
||||
var SoftwareInfo = SoftwareInfoStr{
|
||||
Name: "Nukumizu",
|
||||
Version: "0.1.0",
|
||||
Developer: "Madobi Nanami",
|
||||
BuildVer: 1,
|
||||
CommitHash: "unknown",
|
||||
Description: "Remote server monitoring and command execution subsystem for Komari",
|
||||
BuildType: "Debug",
|
||||
BuildTime: "unknown",
|
||||
}
|
||||
@@ -31,6 +31,15 @@ type Controller interface {
|
||||
SendExecuteResult(serverName, serverUUID, command, result string) error
|
||||
}
|
||||
|
||||
// BotController is implemented by controllers that act as chat bots and can
|
||||
// deliver arbitrary messages, such as the bot initialization message on startup.
|
||||
// Only bot-type pipes (QQ/NapCat, Telegram) implement it; notification-only
|
||||
// pipes (email, ntfy, webhook) do not.
|
||||
type BotController interface {
|
||||
Controller
|
||||
SendMessage(message string) error
|
||||
}
|
||||
|
||||
// Manager manages all controller instances and routes events.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
@@ -60,6 +69,56 @@ func (m *Manager) Register(c Controller) {
|
||||
postLog.Info("Controller registered: " + c.Name())
|
||||
}
|
||||
|
||||
// ShowBotInitMessage sends the bot initialization message to all enabled
|
||||
// bot controllers (QQ/NapCat and Telegram). Notification-only pipes that do
|
||||
// not implement BotController are skipped.
|
||||
func (m *Manager) ShowBotInitMessage() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
cfg := config.GetConfig()
|
||||
params := template.BuildBotInitializationMsgParams()
|
||||
message := template.Render(cfg.ControllerMessage.BotStarted, params)
|
||||
|
||||
for _, ctrl := range m.controllers {
|
||||
if !ctrl.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
bot, ok := ctrl.(BotController)
|
||||
if !ok {
|
||||
continue // Notification-only pipe (email/ntfy/webhook), not a bot.
|
||||
}
|
||||
if err := bot.SendMessage(message); err != nil {
|
||||
postLog.Warning(fmt.Sprintf("Controller %s failed to send init message: %v", bot.Name(), err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShowBotServerList sends the server list to all enabled bot controllers. The
|
||||
// message content is identical to the /list command (same template and
|
||||
// parameters).
|
||||
func (m *Manager) ShowBotServerList() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
cfg := config.GetConfig()
|
||||
params := template.BuildParamsFromServerList()
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
|
||||
for _, ctrl := range m.controllers {
|
||||
if !ctrl.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
bot, ok := ctrl.(BotController)
|
||||
if !ok {
|
||||
continue // Notification-only pipe (email/ntfy/webhook), not a bot.
|
||||
}
|
||||
if err := bot.SendMessage(message); err != nil {
|
||||
postLog.Warning(fmt.Sprintf("Controller %s failed to send server list: %v", bot.Name(), err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyStatusChange sends a status change notification to all enabled controllers.
|
||||
func (m *Manager) NotifyStatusChange(change node.StatusChange) {
|
||||
m.mu.RLock()
|
||||
|
||||
@@ -201,6 +201,22 @@ func (q *QQController) isSelfMessage(ev oneBotEvent) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !q.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, groupID := range q.cfg.TrustedGroups {
|
||||
q.sendGroupMessage(groupID, message)
|
||||
}
|
||||
for _, adminID := range q.cfg.Admins {
|
||||
q.sendPrivateMessage(adminID, message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendStatusChange sends a status change notification via QQ.
|
||||
func (q *QQController) SendStatusChange(change node.StatusChange) error {
|
||||
if !q.cfg.Enabled {
|
||||
|
||||
@@ -92,6 +92,17 @@ func (t *TelegramController) processCommand(cmd controller.Command) string {
|
||||
return response
|
||||
}
|
||||
|
||||
// SendMessage sends an arbitrary message (e.g. the bot initialization message)
|
||||
// via Telegram. The Telegram Bot API integration is currently a stub, so this
|
||||
// only logs the message for now.
|
||||
func (t *TelegramController) SendMessage(message string) error {
|
||||
if !t.cfg.Enabled || t.cfg.BotToken == "" {
|
||||
return nil
|
||||
}
|
||||
postLog.Debug("Telegram init message: " + message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendStatusChange sends a status change notification via Telegram.
|
||||
func (t *TelegramController) SendStatusChange(change node.StatusChange) error {
|
||||
if !t.cfg.Enabled || t.cfg.BotToken == "" {
|
||||
|
||||
@@ -71,11 +71,12 @@ type StatusChangeCallback func(change StatusChange)
|
||||
// Tracker maintains the in-memory state of all monitored nodes.
|
||||
// All methods are thread-safe.
|
||||
type Tracker struct {
|
||||
mu sync.RWMutex
|
||||
nodes map[string]*Node // uuid → Node
|
||||
uuidToName map[string]string // uuid → name (from node list)
|
||||
onlineSet map[string]bool // which uuids are currently online
|
||||
callbacks []StatusChangeCallback
|
||||
mu sync.RWMutex
|
||||
nodes map[string]*Node // uuid → Node
|
||||
uuidToName map[string]string // uuid → name (from node list)
|
||||
onlineSet map[string]bool // which uuids are currently online
|
||||
callbacks []StatusChangeCallback
|
||||
onBootstrapDone []func()
|
||||
|
||||
// Initial-refresh gating: status-change notifications are suppressed until
|
||||
// the initial status snapshot has been received, so the startup state is
|
||||
@@ -250,13 +251,21 @@ func (t *Tracker) UpdateStatus(onlineUUIDs []string, reports map[string]Report)
|
||||
}
|
||||
|
||||
suppressChanges := !t.bootstrapDone || firstSnapshot
|
||||
bootstrapDoneNow := false
|
||||
if !t.bootstrapDone && t.refreshComplete(reports) {
|
||||
t.bootstrapDone = true
|
||||
bootstrapDoneNow = true
|
||||
postLog.Info(fmt.Sprintf("Initial node status refresh complete (%d/%d nodes reported); status notifications enabled",
|
||||
len(t.reportedUUIDs), len(t.knownUUIDs)))
|
||||
}
|
||||
t.mu.Unlock()
|
||||
|
||||
// Notify startup listeners (e.g. server list sender) once the initial
|
||||
// refresh has finished.
|
||||
if bootstrapDoneNow {
|
||||
t.fireBootstrapDone()
|
||||
}
|
||||
|
||||
// Fire callbacks for each change (outside the lock).
|
||||
for _, change := range changes {
|
||||
if suppressChanges {
|
||||
@@ -301,6 +310,40 @@ func (t *Tracker) refreshComplete(currentReports map[string]Report) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// OnBootstrapDone registers a callback invoked once the initial status refresh
|
||||
// completes (e.g. to send the startup server list). If the refresh has already
|
||||
// completed, the callback is invoked immediately. Each callback fires exactly
|
||||
// once.
|
||||
func (t *Tracker) OnBootstrapDone(cb func()) {
|
||||
t.mu.Lock()
|
||||
t.onBootstrapDone = append(t.onBootstrapDone, cb)
|
||||
if !t.bootstrapDone {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
// Already complete — collect everything pending and fire outside the lock.
|
||||
cbs := t.onBootstrapDone
|
||||
t.onBootstrapDone = nil
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, c := range cbs {
|
||||
c()
|
||||
}
|
||||
}
|
||||
|
||||
// fireBootstrapDone notifies all registered callbacks that the initial status
|
||||
// refresh is complete. Must be called with the lock NOT held.
|
||||
func (t *Tracker) fireBootstrapDone() {
|
||||
t.mu.Lock()
|
||||
cbs := t.onBootstrapDone
|
||||
t.onBootstrapDone = nil
|
||||
t.mu.Unlock()
|
||||
|
||||
for _, cb := range cbs {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteBootstrap force-completes the initial status refresh gate. It is a
|
||||
// safety net so that notifications cannot be blocked forever when no status
|
||||
// snapshot is ever received. It does not mark a snapshot as received, so the
|
||||
@@ -308,11 +351,14 @@ func (t *Tracker) refreshComplete(currentReports map[string]Report) bool {
|
||||
// again is a no-op.
|
||||
func (t *Tracker) CompleteBootstrap() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if !t.bootstrapDone {
|
||||
t.bootstrapDone = true
|
||||
postLog.Info("Initial node status refresh timeout reached; status notifications enabled")
|
||||
if t.bootstrapDone {
|
||||
t.mu.Unlock()
|
||||
return
|
||||
}
|
||||
t.bootstrapDone = true
|
||||
postLog.Info("Initial node status refresh timeout reached; status notifications enabled")
|
||||
t.mu.Unlock()
|
||||
t.fireBootstrapDone()
|
||||
}
|
||||
|
||||
// GetNode returns a copy of the node data for the given UUID.
|
||||
|
||||
@@ -5,21 +5,43 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nukumizu-backend/global"
|
||||
"nukumizu-backend/internal/node"
|
||||
)
|
||||
|
||||
// Params holds all possible template parameters.
|
||||
type Params struct {
|
||||
Time string
|
||||
ServerName string
|
||||
ServerUUID string
|
||||
UpStatus string
|
||||
Event string
|
||||
Message string
|
||||
Command string
|
||||
Result string
|
||||
OnlineServers string // Pre-formatted multi-line list
|
||||
OfflineServers string // Pre-formatted multi-line list
|
||||
Time string
|
||||
ServerName string
|
||||
ServerUUID string
|
||||
UpStatus string
|
||||
Event string
|
||||
Message string
|
||||
Command string
|
||||
Result string
|
||||
OnlineServers string // Pre-formatted multi-line list
|
||||
OfflineServers string // Pre-formatted multi-line list
|
||||
SoftwareVersion string
|
||||
SoftwareBuildVer int16
|
||||
SoftwareCommitHash string
|
||||
SoftwareBuildType string
|
||||
SoftwareBuildTime string
|
||||
SoftwareDeveloper string
|
||||
SoftwareDescription string
|
||||
}
|
||||
|
||||
// BuildBotInitializationMsgParams creates template parameters for the bot initialization message.
|
||||
func BuildBotInitializationMsgParams() Params {
|
||||
return Params{
|
||||
Time: time.Now().Format("2006-01-02T15:04:05.000000000-07:00"),
|
||||
SoftwareVersion: global.SoftwareInfo.Version,
|
||||
SoftwareBuildVer: global.SoftwareInfo.BuildVer,
|
||||
SoftwareCommitHash: global.SoftwareInfo.CommitHash,
|
||||
SoftwareBuildType: global.SoftwareInfo.BuildType,
|
||||
SoftwareBuildTime: global.SoftwareInfo.BuildTime,
|
||||
SoftwareDeveloper: global.SoftwareInfo.Developer,
|
||||
SoftwareDescription: global.SoftwareInfo.Description,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildParamsFromStatusChange creates template parameters from a status change event.
|
||||
@@ -70,6 +92,13 @@ func BuildParamsFromExecResult(serverName, serverUUID, command, result string) P
|
||||
// - {{ result }} — command execution result
|
||||
// - {{ list.onlineServers }} — multi-line online server list
|
||||
// - {{ list.offlineServers }} — multi-line offline server list
|
||||
// - {{ softwareVersion }} — software version
|
||||
// - {{ softwareBuildVer }} — software build version
|
||||
// - {{ softwareCommitHash }} — software commit hash
|
||||
// - {{ softwareBuildType }} — software build type (Debug/Release)
|
||||
// - {{ softwareBuildTime }} — software build time
|
||||
// - {{ softwareDeveloper }} — software developer
|
||||
// - {{ softwareDescription }} — software description
|
||||
func Render(tmpl string, params Params) string {
|
||||
result := tmpl
|
||||
|
||||
@@ -83,6 +112,13 @@ func Render(tmpl string, params Params) string {
|
||||
result = strings.ReplaceAll(result, "{{ result }}", params.Result)
|
||||
result = strings.ReplaceAll(result, "{{ list.onlineServers }}", params.OnlineServers)
|
||||
result = strings.ReplaceAll(result, "{{ list.offlineServers }}", params.OfflineServers)
|
||||
result = strings.ReplaceAll(result, "{{ softwareVersion }}", params.SoftwareVersion)
|
||||
result = strings.ReplaceAll(result, "{{ softwareBuildVer }}", fmt.Sprintf("%d", params.SoftwareBuildVer))
|
||||
result = strings.ReplaceAll(result, "{{ softwareCommitHash }}", params.SoftwareCommitHash)
|
||||
result = strings.ReplaceAll(result, "{{ softwareBuildType }}", params.SoftwareBuildType)
|
||||
result = strings.ReplaceAll(result, "{{ softwareBuildTime }}", params.SoftwareBuildTime)
|
||||
result = strings.ReplaceAll(result, "{{ softwareDeveloper }}", params.SoftwareDeveloper)
|
||||
result = strings.ReplaceAll(result, "{{ softwareDescription }}", params.SoftwareDescription)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"nukumizu-backend/config"
|
||||
"nukumizu-backend/database"
|
||||
"nukumizu-backend/global"
|
||||
"nukumizu-backend/internal/controller"
|
||||
"nukumizu-backend/internal/controller/pipes"
|
||||
"nukumizu-backend/internal/controller/pipes/qq_napcat"
|
||||
@@ -27,36 +28,17 @@ var (
|
||||
BuildTime string
|
||||
)
|
||||
|
||||
// SoftwareInfo holds build metadata.
|
||||
type SoftwareInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
Developer string
|
||||
BuildVer int16
|
||||
CommitHash string
|
||||
Description string
|
||||
BuildType string
|
||||
BuildTime string
|
||||
}
|
||||
|
||||
var softwareInfo = SoftwareInfo{
|
||||
Name: "Nukumizu",
|
||||
Version: "0.1.0",
|
||||
Developer: "Madobi Nanami",
|
||||
BuildVer: 1,
|
||||
CommitHash: CommitHash,
|
||||
Description: "Remote server monitoring and command execution subsystem for Komari",
|
||||
BuildType: "Debug",
|
||||
BuildTime: BuildTime,
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Set global software info.
|
||||
global.SoftwareInfo.CommitHash = CommitHash
|
||||
global.SoftwareInfo.BuildTime = BuildTime
|
||||
|
||||
// Parse CLI flags.
|
||||
configPath := flag.String("config", "config.json", "Path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
// Log startup banner.
|
||||
postLog.Info(fmt.Sprintf("%s Ver.%s.%d.%s.%s Developed by %s at %s", softwareInfo.Name, softwareInfo.Version, softwareInfo.BuildVer, softwareInfo.BuildType, softwareInfo.CommitHash, softwareInfo.Developer, softwareInfo.BuildTime))
|
||||
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)
|
||||
@@ -119,6 +101,22 @@ func main() {
|
||||
// --- Initialize controllers ---
|
||||
initControllers()
|
||||
|
||||
// Send the bot initialization message to all enabled bot controllers
|
||||
// (QQ_NapCat, Telegram), then the server list right after it once the
|
||||
// initial status refresh has completed so the list reflects the real
|
||||
// online/offline state.
|
||||
if mgr := controller.GetManager(); mgr != nil {
|
||||
mgr.ShowBotInitMessage()
|
||||
|
||||
if tracker := node.GetTracker(); tracker != nil {
|
||||
tracker.OnBootstrapDone(func() {
|
||||
if m := controller.GetManager(); m != nil {
|
||||
m.ShowBotServerList()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Start background tasks ---
|
||||
startBackgroundTasks(cfg)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user