Files
uptop/internal/alert/alert.go
T
lerko d6f33a4d1f refactor(alert): extract shared HTTPProvider for webhook-based alerts
Discord, Slack, and Webhook providers now use a single HTTPProvider
struct with a PayloadFunc for the only part that differs. Centralizes
response body handling and adds HTTP status code checking (4xx/5xx
now return errors instead of being silently ignored).

Email and Ntfy keep separate implementations (different protocols).
Adding a new HTTP-based alert provider is now a one-line PayloadFunc.
2026-05-15 00:46:05 -04:00

135 lines
3.2 KiB
Go

package alert
import (
"bytes"
"encoding/json"
"fmt"
"go-upkeep/internal/models"
"net/http"
"net/smtp"
"strings"
"time"
)
var alertClient = &http.Client{Timeout: 10 * time.Second}
type Provider interface {
Send(title, message string) error
}
type PayloadFunc func(title, message string) ([]byte, error)
type HTTPProvider struct {
URL string
Payload PayloadFunc
}
func (h *HTTPProvider) Send(title, message string) error {
body, err := h.Payload(title, message)
if err != nil {
return err
}
resp, err := alertClient.Post(h.URL, "application/json", bytes.NewBuffer(body))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("alert webhook returned HTTP %d", resp.StatusCode)
}
return nil
}
func discordPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"content": fmt.Sprintf("**%s**\n%s", title, message)})
}
func slackPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"text": fmt.Sprintf("*%s*\n%s", title, message)})
}
func webhookPayload(title, message string) ([]byte, error) {
return json.Marshal(map[string]string{"title": title, "message": message, "status": "alert"})
}
func GetProvider(cfg models.AlertConfig) Provider {
switch cfg.Type {
case "discord":
return &HTTPProvider{URL: cfg.Settings["url"], Payload: discordPayload}
case "slack":
return &HTTPProvider{URL: cfg.Settings["url"], Payload: slackPayload}
case "webhook":
return &HTTPProvider{URL: cfg.Settings["url"], Payload: webhookPayload}
case "email":
port := "25"
if p, ok := cfg.Settings["port"]; ok {
port = p
}
return &EmailProvider{
Host: cfg.Settings["host"],
Port: port,
User: cfg.Settings["user"],
Pass: cfg.Settings["pass"],
To: cfg.Settings["to"],
From: cfg.Settings["from"],
}
case "ntfy":
priority := "3"
if p, ok := cfg.Settings["priority"]; ok && p != "" {
priority = p
}
return &NtfyProvider{
ServerURL: cfg.Settings["url"],
Topic: cfg.Settings["topic"],
Priority: priority,
Username: cfg.Settings["username"],
Password: cfg.Settings["password"],
}
default:
return nil
}
}
type EmailProvider struct {
Host, Port, User, Pass, To, From string
}
func (e *EmailProvider) Send(title, message string) error {
auth := smtp.PlainAuth("", e.User, e.Pass, e.Host)
msg := []byte("To: " + e.To + "\r\n" +
"Subject: Go-Upkeep: " + title + "\r\n" +
"\r\n" +
message + "\r\n")
return smtp.SendMail(e.Host+":"+e.Port, auth, e.From, []string{e.To}, msg)
}
type NtfyProvider struct {
ServerURL string
Topic string
Priority string
Username string
Password string
}
func (n *NtfyProvider) Send(title, message string) error {
url := strings.TrimRight(n.ServerURL, "/") + "/" + n.Topic
req, err := http.NewRequest("POST", url, strings.NewReader(message))
if err != nil {
return err
}
req.Header.Set("Title", title)
req.Header.Set("Priority", n.Priority)
if n.Username != "" && n.Password != "" {
req.SetBasicAuth(n.Username, n.Password)
}
resp, err := alertClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("ntfy returned HTTP %d", resp.StatusCode)
}
return nil
}