feat: add incoming webhook API support with configurable endpoints
- Implemented the incoming webhook API to handle alerts from external applications. - Added configuration options for webhook listening address, port, and endpoints in config.go. - Created WebhookReceiverConfig and WebhookEndpointConfig structures to manage webhook settings. - Developed WebhookHandler to process incoming requests, validate tokens, and deliver alerts to specified channels. - Enhanced existing controller interfaces to support alert delivery. - Updated message rendering to respect Markdown settings for different channels. - Added tests for webhook functionality and ensured proper error handling.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
gomail "gopkg.in/mail.v2"
|
||||
|
||||
"nukumizu-backend/config"
|
||||
"nukumizu-backend/internal/controller"
|
||||
"nukumizu-backend/internal/netproxy"
|
||||
"nukumizu-backend/internal/node"
|
||||
"nukumizu-backend/internal/template"
|
||||
@@ -54,6 +55,12 @@ func (e *EmailController) IsEnabled() bool {
|
||||
return e.cfg.Enabled
|
||||
}
|
||||
|
||||
// IsMarkdown returns whether the channel renders Markdown, per its markdown
|
||||
// setting in config.json.
|
||||
func (e *EmailController) IsMarkdown() bool {
|
||||
return e.cfg.Markdown
|
||||
}
|
||||
|
||||
// SendStatusChange sends a status change notification via Email.
|
||||
func (e *EmailController) SendStatusChange(change node.StatusChange) error {
|
||||
if !e.cfg.Enabled {
|
||||
@@ -66,7 +73,7 @@ func (e *EmailController) SendStatusChange(change node.StatusChange) error {
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromStatusChange(change)
|
||||
body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params)
|
||||
body := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, e.cfg.Markdown)
|
||||
|
||||
subject := fmt.Sprintf("Server Status Change: %s - %s", change.Name, change.Event)
|
||||
return e.sendEmail(subject, body)
|
||||
@@ -80,7 +87,7 @@ func (e *EmailController) SendServerList(onlineServers, offlineServers string) e
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
body := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
body := template.Render(cfg.ControllerMessage.ServerList, params, e.cfg.Markdown)
|
||||
|
||||
return e.sendEmail("Server List", body)
|
||||
}
|
||||
@@ -93,12 +100,26 @@ func (e *EmailController) SendExecuteResult(serverName, serverUUID, command, res
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||
body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params)
|
||||
body := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, e.cfg.Markdown)
|
||||
|
||||
subject := fmt.Sprintf("Command Result: %s on %s", command, serverName)
|
||||
return e.sendEmail(subject, body)
|
||||
}
|
||||
|
||||
// SendAlert sends an alert submitted through the incoming webhook API to the
|
||||
// configured recipients.
|
||||
func (e *EmailController) SendAlert(alert controller.Alert) error {
|
||||
if !e.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
if len(e.cfg.To) == 0 {
|
||||
postLog.Debug("Email controller has no recipients configured")
|
||||
return nil
|
||||
}
|
||||
|
||||
return e.sendEmail(alert.Subject, alert.Render(e.cfg.Markdown))
|
||||
}
|
||||
|
||||
func (e *EmailController) sendEmail(subject, body string) error {
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", e.cfg.From)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"nukumizu-backend/config"
|
||||
"nukumizu-backend/internal/controller"
|
||||
"nukumizu-backend/internal/netproxy"
|
||||
"nukumizu-backend/internal/node"
|
||||
"nukumizu-backend/internal/template"
|
||||
@@ -52,6 +53,12 @@ func (n *NtfyController) IsEnabled() bool {
|
||||
return n.cfg.Enabled
|
||||
}
|
||||
|
||||
// IsMarkdown returns whether the channel renders Markdown, per its markdown
|
||||
// setting in config.json.
|
||||
func (n *NtfyController) IsMarkdown() bool {
|
||||
return n.cfg.Markdown
|
||||
}
|
||||
|
||||
// SendStatusChange sends a status change notification via Ntfy.
|
||||
func (n *NtfyController) SendStatusChange(change node.StatusChange) error {
|
||||
if !n.cfg.Enabled {
|
||||
@@ -60,7 +67,7 @@ func (n *NtfyController) SendStatusChange(change node.StatusChange) error {
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromStatusChange(change)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, n.cfg.Markdown)
|
||||
|
||||
title := fmt.Sprintf("Server %s: %s", change.Name, change.Event)
|
||||
return n.publish(title, message)
|
||||
@@ -74,7 +81,7 @@ func (n *NtfyController) SendServerList(onlineServers, offlineServers string) er
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params, n.cfg.Markdown)
|
||||
|
||||
return n.publish("Server List", message)
|
||||
}
|
||||
@@ -87,12 +94,22 @@ func (n *NtfyController) SendExecuteResult(serverName, serverUUID, command, resu
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, n.cfg.Markdown)
|
||||
|
||||
title := fmt.Sprintf("Command Result: %s on %s", command, serverName)
|
||||
return n.publish(title, message)
|
||||
}
|
||||
|
||||
// SendAlert sends an alert submitted through the incoming webhook API to the
|
||||
// configured topic.
|
||||
func (n *NtfyController) SendAlert(alert controller.Alert) error {
|
||||
if !n.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return n.publish(alert.Subject, alert.Render(n.cfg.Markdown))
|
||||
}
|
||||
|
||||
func (n *NtfyController) publish(title, message string) error {
|
||||
serverURL := n.cfg.Server
|
||||
if serverURL == "" {
|
||||
|
||||
@@ -86,6 +86,12 @@ func (q *QQController) IsEnabled() bool {
|
||||
return q.cfg.Enabled
|
||||
}
|
||||
|
||||
// IsMarkdown returns whether the channel renders Markdown, per its markdown
|
||||
// setting in config.json.
|
||||
func (q *QQController) IsMarkdown() bool {
|
||||
return q.cfg.Markdown
|
||||
}
|
||||
|
||||
// handleNapcatEvent processes a raw OneBot event received from the NapCat WebSocket.
|
||||
func (q *QQController) handleNapcatEvent(raw []byte) {
|
||||
var ev oneBotEvent
|
||||
@@ -179,7 +185,7 @@ func (q *QQController) processCommand(cmd controller.Command) string {
|
||||
parsed.ChatID = cmd.ChatID
|
||||
parsed.ChatType = cmd.ChatType
|
||||
parsed.SenderID = cmd.SenderID
|
||||
parsed.Source = "qq_napcat"
|
||||
parsed.Source = q.Name()
|
||||
|
||||
// Hand the complete command to the unified processor, which checks group
|
||||
// vs private, trusted groups, admin permissions, and executes it.
|
||||
@@ -256,7 +262,7 @@ func (q *QQController) SendStatusChange(change node.StatusChange) error {
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromStatusChange(change)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, q.cfg.Markdown)
|
||||
|
||||
// Only notify trusted groups and admins whose event_status_notify is true.
|
||||
if uc := config.C_botUserConfig; uc != nil {
|
||||
@@ -285,7 +291,7 @@ func (q *QQController) SendServerList(onlineServers, offlineServers string) erro
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params, q.cfg.Markdown)
|
||||
|
||||
for _, groupID := range q.trustedGroupIDs() {
|
||||
q.sendGroupMessage(groupID, message)
|
||||
@@ -301,7 +307,7 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, q.cfg.Markdown)
|
||||
|
||||
for _, groupID := range q.trustedGroupIDs() {
|
||||
q.sendGroupMessage(groupID, message)
|
||||
@@ -309,6 +315,20 @@ func (q *QQController) SendExecuteResult(serverName, serverUUID, command, result
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAlert sends an alert submitted through the incoming webhook API to all QQ
|
||||
// trusted groups and admins.
|
||||
func (q *QQController) SendAlert(alert controller.Alert) error {
|
||||
if !q.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return q.SendMessage(controller.Message{
|
||||
Source: q.Name(),
|
||||
Content: alert.Render(q.cfg.Markdown),
|
||||
Type: controller.MessageTypeAlert,
|
||||
})
|
||||
}
|
||||
|
||||
func (q *QQController) sendGroupMessage(groupID string, message string) {
|
||||
if q.napcatClient == nil {
|
||||
postLog.Warning("Cannot send QQ group message: NapCat client not initialized")
|
||||
|
||||
@@ -14,11 +14,13 @@ import (
|
||||
const maxMessageLen = 4000
|
||||
|
||||
// sendMessage sends a text message to a chat, splitting it into chunks that fit
|
||||
// Telegram's 4096-character limit. All messages are sent with
|
||||
// parse_mode=Markdown so fenced code blocks and inline formatting render as
|
||||
// rich text. Templates must stay valid under Telegram's legacy Markdown:
|
||||
// unpaired '*' or '_' characters (e.g. a lone '*Event: ...' label) make the
|
||||
// API reject the whole message.
|
||||
// Telegram's 4096-character limit. When the channel has markdown enabled the
|
||||
// message is sent with parse_mode=Markdown so fenced code blocks and inline
|
||||
// formatting render as rich text; templates must then stay valid under
|
||||
// Telegram's legacy Markdown, because unpaired '*' or '_' characters (e.g. a
|
||||
// lone '*Event: ...' label) make the API reject the whole message. With
|
||||
// markdown disabled the message is sent without a parse mode, so it is
|
||||
// delivered verbatim whatever it contains.
|
||||
func (t *TelegramController) sendMessage(message controller.Message) error {
|
||||
if t.client == nil {
|
||||
return nil
|
||||
@@ -40,11 +42,15 @@ func (t *TelegramController) sendMessageChunk(chatID int64, text string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, err := t.client.SendMessage(ctx, &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
ParseMode: models.ParseModeMarkdownV1, // Telegram legacy Markdown
|
||||
})
|
||||
params := &bot.SendMessageParams{
|
||||
ChatID: chatID,
|
||||
Text: text,
|
||||
}
|
||||
if t.cfg.Markdown {
|
||||
params.ParseMode = models.ParseModeMarkdownV1 // Telegram legacy Markdown
|
||||
}
|
||||
|
||||
_, err := t.client.SendMessage(ctx, params)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,12 @@ func (t *TelegramController) IsEnabled() bool {
|
||||
return t.cfg.Enabled
|
||||
}
|
||||
|
||||
// IsMarkdown returns whether the channel renders Markdown, per its markdown
|
||||
// setting in config.json.
|
||||
func (t *TelegramController) IsMarkdown() bool {
|
||||
return t.cfg.Markdown
|
||||
}
|
||||
|
||||
// handleUpdate processes a single Telegram update received via long polling. It
|
||||
// is installed as the framework's default handler (every update with a Message
|
||||
// reaches it). Updates are processed sequentially because the bot is created
|
||||
@@ -271,7 +277,7 @@ func (t *TelegramController) SendStatusChange(change node.StatusChange) error {
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromStatusChange(change)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, t.cfg.Markdown)
|
||||
|
||||
// Only notify trusted groups and admins whose event_status_notify is true.
|
||||
if uc := config.C_botUserConfig; uc != nil {
|
||||
@@ -299,7 +305,7 @@ func (t *TelegramController) SendServerList(onlineServers, offlineServers string
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params, t.cfg.Markdown)
|
||||
|
||||
t.sendToGroups(message)
|
||||
return nil
|
||||
@@ -313,12 +319,26 @@ func (t *TelegramController) SendExecuteResult(serverName, serverUUID, command,
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, t.cfg.Markdown)
|
||||
|
||||
t.sendToGroups(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendAlert sends an alert submitted through the incoming webhook API to all
|
||||
// Telegram trusted groups and admins.
|
||||
func (t *TelegramController) SendAlert(alert controller.Alert) error {
|
||||
if !t.cfg.Enabled || t.client == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return t.SendMessage(controller.Message{
|
||||
Source: t.Name(),
|
||||
Content: alert.Render(t.cfg.Markdown),
|
||||
Type: controller.MessageTypeAlert,
|
||||
})
|
||||
}
|
||||
|
||||
// telegramChatType maps a Telegram chat type to the unified ChatType value used
|
||||
// by the controller package. Empty means the chat type is unsupported.
|
||||
func telegramChatType(chatType string) string {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"nukumizu-backend/config"
|
||||
"nukumizu-backend/internal/controller"
|
||||
"nukumizu-backend/internal/netproxy"
|
||||
"nukumizu-backend/internal/node"
|
||||
"nukumizu-backend/internal/template"
|
||||
@@ -53,6 +54,12 @@ func (w *WebhookController) IsEnabled() bool {
|
||||
return w.cfg.Enabled
|
||||
}
|
||||
|
||||
// IsMarkdown returns whether the channel renders Markdown, per its markdown
|
||||
// setting in config.json.
|
||||
func (w *WebhookController) IsMarkdown() bool {
|
||||
return w.cfg.Markdown
|
||||
}
|
||||
|
||||
// SendStatusChange sends a status change notification via Webhook.
|
||||
func (w *WebhookController) SendStatusChange(change node.StatusChange) error {
|
||||
if !w.cfg.Enabled {
|
||||
@@ -61,7 +68,7 @@ func (w *WebhookController) SendStatusChange(change node.StatusChange) error {
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromStatusChange(change)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerStatusChanged, params, w.cfg.Markdown)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"event": change.Event,
|
||||
@@ -82,14 +89,14 @@ func (w *WebhookController) SendServerList(onlineServers, offlineServers string)
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerList, params, w.cfg.Markdown)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "serverList",
|
||||
"onlineServers": params.OnlineServers,
|
||||
"offlineServers": params.OfflineServers,
|
||||
"message": message,
|
||||
"time": params.Time,
|
||||
"type": "serverList",
|
||||
"onlineServers": params.OnlineServers,
|
||||
"offlineServers": params.OfflineServers,
|
||||
"message": message,
|
||||
"time": params.Time,
|
||||
}
|
||||
|
||||
return w.send(payload)
|
||||
@@ -103,7 +110,7 @@ func (w *WebhookController) SendExecuteResult(serverName, serverUUID, command, r
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromExecResult(serverName, serverUUID, command, result)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params)
|
||||
message := template.Render(cfg.ControllerMessage.ServerExecuteResult, params, w.cfg.Markdown)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "executeResult",
|
||||
@@ -118,6 +125,25 @@ func (w *WebhookController) SendExecuteResult(serverName, serverUUID, command, r
|
||||
return w.send(payload)
|
||||
}
|
||||
|
||||
// SendAlert sends an alert submitted through the incoming webhook API to the
|
||||
// configured URL.
|
||||
func (w *WebhookController) SendAlert(alert controller.Alert) error {
|
||||
if !w.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "alert",
|
||||
"subject": alert.Subject,
|
||||
"source": alert.Source,
|
||||
"content": alert.Content,
|
||||
"message": alert.Render(w.cfg.Markdown),
|
||||
"time": alert.Time,
|
||||
}
|
||||
|
||||
return w.send(payload)
|
||||
}
|
||||
|
||||
func (w *WebhookController) send(payload map[string]interface{}) error {
|
||||
method := w.cfg.Method
|
||||
if method == "" {
|
||||
|
||||
Reference in New Issue
Block a user