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:
2026-09-24 11:21:38 +08:00
parent 90ca08066b
commit 057d7d584d
17 changed files with 595 additions and 65 deletions
+63 -12
View File
@@ -19,6 +19,9 @@ type Params struct {
Message string
Command string
Result string
Subject string // Alert subject (see AlertParams)
Source string // Alert source (see AlertParams)
Content string // Alert content (see AlertParams)
OnlineServers string // Pre-formatted multi-line list
OfflineServers string // Pre-formatted multi-line list
SoftwareVersion string
@@ -30,6 +33,37 @@ type Params struct {
SoftwareDescription string
}
// AlertTemplate is the body format of an alert submitted by an external
// application through the incoming webhook API.
const AlertTemplate = "{{ subject }}\n- Source: {{ source }}\n- Content:\n{{ content }}\n\n- Time: {{ time }}\nSent by Nukumizu Alert System"
// AlertParams holds the parameters of an alert submitted through the incoming
// webhook API.
type AlertParams 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
}
// RenderAlert renders the body of an alert for a channel. The alert source and
// content may be wrapped in Markdown — the source in inline code, the content
// in a fenced code block — when the target channel has markdown enabled
// (markdown); everything else, the timestamp included, stays plain text.
func RenderAlert(alert AlertParams, markdown bool) string {
params := Params{
Time: alert.Time,
Subject: alert.Subject,
Source: alert.Source,
Content: alert.Content,
}
if markdown {
params.Source = "`" + params.Source + "`"
params.Content = "```\n" + params.Content + "\n```"
}
return Render(AlertTemplate, params, false)
}
// BuildBotInitializationMsgParams creates template parameters for the bot initialization message.
func BuildBotInitializationMsgParams() Params {
return Params{
@@ -80,7 +114,13 @@ func BuildParamsFromExecResult(serverName, serverUUID, command, result string) P
}
}
// Render substitutes {{ paramName }} placeholders in a template string.
// Render substitutes {{ paramName }} placeholders in a template string. The
// markdown argument is the target channel's markdown setting: when true the
// values that are meant to be read verbatim (UUIDs, messages, commands, command
// results) are wrapped in Markdown code spans and blocks, otherwise every value
// is inserted as plain text. Whether a channel renders Markdown comes from the
// configuration alone — the renderer never infers it from the channel name.
//
// Supported placeholders:
// - {{ time }} — current server time
// - {{ serverName }} — server name
@@ -90,6 +130,9 @@ func BuildParamsFromExecResult(serverName, serverUUID, command, result string) P
// - {{ message }} — event descriptive message
// - {{ command }} — executed command
// - {{ result }} — command execution result
// - {{ subject }} — alert subject
// - {{ source }} — alert source
// - {{ content }} — alert content
// - {{ list.onlineServers }} — multi-line online server list
// - {{ list.offlineServers }} — multi-line offline server list
// - {{ softwareVersion }} — software version
@@ -99,19 +142,20 @@ func BuildParamsFromExecResult(serverName, serverUUID, command, result string) P
// - {{ softwareBuildTime }} — software build time
// - {{ softwareDeveloper }} — software developer
// - {{ softwareDescription }} — software description
func Render(tmpl string, params Params, source ...string) string {
func Render(tmpl string, params Params, markdown bool) string {
result := tmpl
if len(source) > 0 && source[0] == "telegram" {
// Telegram requires special formatting for code blocks and inline code.
result = strings.ReplaceAll(result, "{{ time }}", "**" + params.Time + "**")
result = strings.ReplaceAll(result, "{{ serverName }}", "**" + params.ServerName + "**")
result = strings.ReplaceAll(result, "{{ serverUUID }}", "`" + params.ServerUUID + "`")
result = strings.ReplaceAll(result, "{{ upStatus }}", "**" + params.UpStatus + "**")
result = strings.ReplaceAll(result, "{{ event }}", "**" + params.Event + "**")
result = strings.ReplaceAll(result, "{{ message }}", "`" + params.Message + "`")
result = strings.ReplaceAll(result, "{{ command }}", "`" + params.Command + "`")
result = strings.ReplaceAll(result, "{{ result }}", "```bash\n" + params.Result + "\n```")
if markdown {
// Channels that render Markdown get code blocks and inline code for the
// values that are read verbatim.
result = strings.ReplaceAll(result, "{{ time }}", "**"+params.Time+"**")
result = strings.ReplaceAll(result, "{{ serverName }}", "**"+params.ServerName+"**")
result = strings.ReplaceAll(result, "{{ serverUUID }}", "`"+params.ServerUUID+"`")
result = strings.ReplaceAll(result, "{{ upStatus }}", "**"+params.UpStatus+"**")
result = strings.ReplaceAll(result, "{{ event }}", "**"+params.Event+"**")
result = strings.ReplaceAll(result, "{{ message }}", "`"+params.Message+"`")
result = strings.ReplaceAll(result, "{{ command }}", "`"+params.Command+"`")
result = strings.ReplaceAll(result, "{{ result }}", "```bash\n"+params.Result+"\n```")
result = strings.ReplaceAll(result, "{{ list.onlineServers }}", params.OnlineServers)
result = strings.ReplaceAll(result, "{{ list.offlineServers }}", params.OfflineServers)
result = strings.ReplaceAll(result, "{{ softwareVersion }}", params.SoftwareVersion)
@@ -140,6 +184,13 @@ func Render(tmpl string, params Params, source ...string) string {
result = strings.ReplaceAll(result, "{{ softwareDeveloper }}", params.SoftwareDeveloper)
result = strings.ReplaceAll(result, "{{ softwareDescription }}", params.SoftwareDescription)
}
// Alert values carry their own formatting (see RenderAlert), so they are
// substituted identically in both branches.
result = strings.ReplaceAll(result, "{{ subject }}", params.Subject)
result = strings.ReplaceAll(result, "{{ source }}", params.Source)
result = strings.ReplaceAll(result, "{{ content }}", params.Content)
return result
}