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:
@@ -1,7 +1,9 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"nukumizu-backend/config"
|
||||
@@ -12,7 +14,7 @@ import (
|
||||
|
||||
// Command represents a parsed bot command.
|
||||
type Command struct {
|
||||
Source string // The source pipe (e.g., "telegram", "qq", "napcat")
|
||||
Source string // Name of the pipe the command arrived on (see Controller.Name)
|
||||
RawText string // The raw text of the command message
|
||||
Command string // The command word (e.g., "list", "status")
|
||||
Args []string // Command arguments
|
||||
@@ -38,8 +40,33 @@ const (
|
||||
// MessageTypeReply marks a direct reply to a user command. Reserved for the
|
||||
// BotUserOptions.EventReply opt-out.
|
||||
MessageTypeReply = "event_reply"
|
||||
// MessageTypeAlert marks an alert submitted by an external application
|
||||
// through the incoming webhook API. Not member-controllable: an alert is
|
||||
// always delivered to the channel's recipients.
|
||||
MessageTypeAlert = "alert"
|
||||
)
|
||||
|
||||
// Alert is a free-form notification submitted by an external application
|
||||
// through the incoming webhook API. Its target channels are chosen per webhook
|
||||
// endpoint in config.json, not per alert.
|
||||
type Alert struct {
|
||||
Subject string // Short one-line title of the alert
|
||||
Source string // Name of the webhook endpoint the alert was submitted to
|
||||
Content string // Free-form alert body
|
||||
Time string // Submission time
|
||||
}
|
||||
|
||||
// Render renders the alert body for a channel, wrapping the source and content
|
||||
// in Markdown when that channel has markdown enabled (see template.RenderAlert).
|
||||
func (a Alert) Render(markdown bool) string {
|
||||
return template.RenderAlert(template.AlertParams{
|
||||
Subject: a.Subject,
|
||||
Source: a.Source,
|
||||
Content: a.Content,
|
||||
Time: a.Time,
|
||||
}, markdown)
|
||||
}
|
||||
|
||||
// MemberReceives reports whether a member whose bot_user_config.json options are
|
||||
// opts receives an automatic message of the given type. Only member-controllable
|
||||
// types are gated; anything else is always delivered.
|
||||
@@ -58,9 +85,15 @@ type Controller interface {
|
||||
Start() error
|
||||
Stop()
|
||||
IsEnabled() bool
|
||||
// IsMarkdown reports whether the channel renders Markdown, per its own
|
||||
// "markdown" setting in config.json.
|
||||
IsMarkdown() bool
|
||||
SendStatusChange(change node.StatusChange) error
|
||||
SendServerList(onlineServers, offlineServers string) error
|
||||
SendExecuteResult(serverName, serverUUID, command, result string) error
|
||||
// SendAlert delivers a free-form alert submitted through the incoming
|
||||
// webhook API to the channel's own recipients.
|
||||
SendAlert(alert Alert) error
|
||||
}
|
||||
|
||||
// BotController is implemented by controllers that act as chat bots and can
|
||||
@@ -105,14 +138,14 @@ func (m *Manager) Register(c Controller) {
|
||||
// bot controllers (QQ/NapCat and Telegram). Notification-only pipes that do
|
||||
// not implement BotController are skipped. The message is typed
|
||||
// MessageTypeBotStarted so each controller can honor its members' per-recipient
|
||||
// EventBotStarted opt-out.
|
||||
// EventBotStarted opt-out. It is rendered once per controller because the
|
||||
// Markdown formatting depends on each channel's own markdown setting.
|
||||
func (m *Manager) ShowBotInitMessage() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildBotInitializationMsgParams()
|
||||
content := template.Render(cfg.ControllerMessage.BotStarted, params)
|
||||
|
||||
for _, ctrl := range m.controllers {
|
||||
if !ctrl.IsEnabled() {
|
||||
@@ -124,7 +157,7 @@ func (m *Manager) ShowBotInitMessage() {
|
||||
}
|
||||
message := Message{
|
||||
Source: bot.Name(),
|
||||
Content: content,
|
||||
Content: template.Render(cfg.ControllerMessage.BotStarted, params, ctrl.IsMarkdown()),
|
||||
Type: MessageTypeBotStarted,
|
||||
}
|
||||
if err := bot.SendMessage(message); err != nil {
|
||||
@@ -137,14 +170,14 @@ func (m *Manager) ShowBotInitMessage() {
|
||||
// controllers. The message content is identical to the /list command (same
|
||||
// template and parameters). Like the init message it is typed
|
||||
// MessageTypeBotStarted so members who opted out of bot-started pushes do not
|
||||
// receive it.
|
||||
// receive it, and rendered once per controller so each channel's markdown
|
||||
// setting is honored.
|
||||
func (m *Manager) ShowBotServerList() {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
cfg := config.C_globalConfig
|
||||
params := template.BuildParamsFromServerList()
|
||||
content := template.Render(cfg.ControllerMessage.ServerList, params)
|
||||
|
||||
for _, ctrl := range m.controllers {
|
||||
if !ctrl.IsEnabled() {
|
||||
@@ -156,7 +189,7 @@ func (m *Manager) ShowBotServerList() {
|
||||
}
|
||||
message := Message{
|
||||
Source: bot.Name(),
|
||||
Content: content,
|
||||
Content: template.Render(cfg.ControllerMessage.ServerList, params, ctrl.IsMarkdown()),
|
||||
Type: MessageTypeBotStarted,
|
||||
}
|
||||
if err := bot.SendMessage(message); err != nil {
|
||||
@@ -188,6 +221,59 @@ func (m *Manager) NotifyStatusChange(change node.StatusChange) {
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyAlert delivers an alert to the named pipes only, and returns the names
|
||||
// of the pipes it was handed to. A pipe that is unknown, disabled or fails to
|
||||
// send is reported through the returned error instead of stopping the delivery
|
||||
// to the remaining pipes; if no pipe accepted the alert, the error describes
|
||||
// every failure.
|
||||
func (m *Manager) NotifyAlert(pipes []string, alert Alert) ([]string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var delivered, failures []string
|
||||
for _, name := range pipes {
|
||||
ctrl, ok := m.controllers[name]
|
||||
if !ok {
|
||||
failures = append(failures, fmt.Sprintf("%s: no such channel", name))
|
||||
continue
|
||||
}
|
||||
if !ctrl.IsEnabled() {
|
||||
failures = append(failures, fmt.Sprintf("%s: channel is disabled", name))
|
||||
continue
|
||||
}
|
||||
if err := ctrl.SendAlert(alert); err != nil {
|
||||
failures = append(failures, fmt.Sprintf("%s: %v", name, err))
|
||||
continue
|
||||
}
|
||||
delivered = append(delivered, name)
|
||||
}
|
||||
|
||||
if len(failures) > 0 {
|
||||
postLog.Warning(fmt.Sprintf("Alert %q from %s not delivered by: %s", alert.Subject, alert.Source, strings.Join(failures, "; ")))
|
||||
}
|
||||
if len(delivered) == 0 {
|
||||
if len(failures) == 0 {
|
||||
return nil, errors.New("no notify channel configured")
|
||||
}
|
||||
return nil, errors.New(strings.Join(failures, "; "))
|
||||
}
|
||||
return delivered, nil
|
||||
}
|
||||
|
||||
// IsMarkdown reports whether the pipe with the given name renders Markdown, per
|
||||
// its channel's "markdown" setting in config.json. An unknown pipe renders
|
||||
// plain text.
|
||||
func (m *Manager) IsMarkdown(pipeName string) bool {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
ctrl, ok := m.controllers[pipeName]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ctrl.IsMarkdown()
|
||||
}
|
||||
|
||||
// StopAll stops all registered controllers.
|
||||
func (m *Manager) StopAll() {
|
||||
m.mu.RLock()
|
||||
|
||||
Reference in New Issue
Block a user