package opsnotify import ( "fmt" "html" "strings" ) // Chat transports colour by severity. Discord takes a decimal integer, Slack // takes a hex string on the attachment. func severityColorInt(s Severity) int { switch s { case SeverityUrgent: return 0xDC2626 // red-600 case SeverityWarning: return 0xD97706 // amber-600 default: return 0x0284C7 // sky-600 } } func severityColorHex(s Severity) string { return fmt.Sprintf("#%06X", severityColorInt(s)) } // discordPayload builds a single embed. Discord caps a field value at 1024 // characters and an embed at 25 fields; both are enforced here so a long // value cannot make the whole POST fail. func discordPayload(e Event) map[string]any { fields := make([]map[string]any, 0, len(e.Fields)) for i, f := range e.Fields { if i >= 25 { break } fields = append(fields, map[string]any{ "name": truncate(f.Label, 256), "value": truncate(emptyDash(f.Value), 1024), "inline": len(f.Value) <= 40, }) } embed := map[string]any{ "title": truncate(e.Title, 256), "description": truncate(e.Summary, 4096), "color": severityColorInt(e.Severity), "footer": map[string]any{"text": "Warmbly"}, } if len(fields) > 0 { embed["fields"] = fields } if e.Link != "" { embed["url"] = e.Link } return map[string]any{"embeds": []any{embed}} } // slackPayload uses an attachment so the severity colour shows as the bar on // the left. The `text` fallback is what a notification preview renders. func slackPayload(e Event) map[string]any { var sb strings.Builder sb.WriteString("*" + e.Title + "*") if e.Summary != "" { sb.WriteString("\n" + e.Summary) } for _, f := range e.Fields { sb.WriteString("\n• *" + f.Label + ":* " + emptyDash(f.Value)) } if e.Link != "" { sb.WriteString("\n<" + e.Link + "|Open in the admin panel>") } return map[string]any{ "text": e.Title, "attachments": []any{ map[string]any{ "color": severityColorHex(e.Severity), "text": sb.String(), "mrkdwn_in": []string{"text"}, }, }, } } func emailSubject(e Event) string { prefix := "[Warmbly]" if e.Severity == SeverityUrgent { prefix = "[Warmbly] Urgent:" } return prefix + " " + e.Title } // emailBodyHTML renders the alert. The transport derives the plain-text part // from this, so it stays simple and table-free. func emailBodyHTML(e Event) string { var sb strings.Builder sb.WriteString(`
` + esc(e.Title) + `
`) if e.Summary != "" { sb.WriteString(`` + esc(e.Summary) + `
`) } if len(e.Fields) > 0 { sb.WriteString(`You are receiving this because this address is an operator notification channel on your Warmbly instance.
`) sb.WriteString(`