Files
Vetting/internal/notify/discord.go
T
josh 9bb4b09a04
CI / Lint + build + test (push) Has been cancelled
Initial commit: full Phases 1-6 implementation
Post-repair hardware validation pipeline for Proxmox cluster hosts.
Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq
PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
2026-04-17 21:32:10 -04:00

88 lines
2.1 KiB
Go

package notify
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// DiscordNotifier posts to a Discord incoming webhook. Body is rendered
// as a single embed so Discord shows a colored sidebar matching event
// severity. Discord rejects empty content+embeds; we always include the
// embed so that never happens.
type DiscordNotifier struct {
NameStr string
WebhookURL string
HTTP *http.Client
}
func NewDiscord(name, webhookURL string) *DiscordNotifier {
return &DiscordNotifier{
NameStr: name,
WebhookURL: webhookURL,
HTTP: &http.Client{Timeout: 10 * time.Second},
}
}
func (d *DiscordNotifier) Name() string { return d.NameStr }
type discordPayload struct {
Embeds []discordEmbed `json:"embeds"`
}
type discordEmbed struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
Color int `json:"color,omitempty"`
}
func (d *DiscordNotifier) Send(ctx context.Context, ev Event) error {
if d.WebhookURL == "" {
return fmt.Errorf("discord: no webhook_url configured")
}
payload := discordPayload{Embeds: []discordEmbed{{
Title: ev.Title,
Description: ev.Body,
URL: ev.URL,
Color: discordColor(ev.Severity),
}}}
buf, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.WebhookURL, bytes.NewReader(buf))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err := d.HTTP.Do(req)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("discord: %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
}
return nil
}
// discordColor returns the embed sidebar color for each severity.
// Values are standard Discord decimal color codes.
func discordColor(s Severity) int {
switch s {
case SeverityCritical:
return 0xE74C3C // red
case SeverityWarning:
return 0xF1C40F // yellow
default:
return 0x2ECC71 // green
}
}