Files

127 lines
3.6 KiB
Go

package notify
import "bytes"
import "context"
import "encoding/json"
import "fmt"
import "io"
import "net/http"
import "strings"
import "codit/internal/models"
// severityHexColor maps an event severity to a hex accent (Teams themeColor,
// Slack attachment color).
func severityHexColor(sev string) string {
switch sev {
case models.EventSeverityCritical:
return "D93025"
case models.EventSeverityWarning:
return "F9AB00"
}
return "1A73E8"
}
// eventText is the human one-liner body shared by the chat transports.
func eventText(event models.Event) string {
var text string
text = event.Title
if strings.TrimSpace(event.Body) != "" {
text = text + "\n" + event.Body
}
return text
}
// postJSON POSTs a JSON payload to url with the shared (system-roots, egress-
// guarded) client and treats any >=400 status as a delivery failure.
func (d *Dispatcher) postJSON(ctx context.Context, channelName string, url string, payload any) error {
var body []byte
var req *http.Request
var resp *http.Response
var cancel context.CancelFunc
var err error
ctx, cancel = context.WithTimeout(ctx, d.webhookTimeout())
defer cancel()
if strings.TrimSpace(url) == "" {
return fmt.Errorf("channel %q has no webhook URL", channelName)
}
body, err = json.Marshal(payload)
if err != nil {
return err
}
req, err = http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimSpace(url), bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
resp, err = d.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
if resp.StatusCode >= 400 {
return fmt.Errorf("channel %q returned status %d", channelName, resp.StatusCode)
}
return nil
}
// teamsMessageCard is the legacy Office 365 connector card format accepted by
// Microsoft Teams incoming webhooks.
type teamsMessageCard struct {
Type string `json:"@type"`
Context string `json:"@context"`
ThemeColor string `json:"themeColor"`
Summary string `json:"summary"`
Title string `json:"title"`
Text string `json:"text"`
}
func buildTeamsPayload(event models.Event) teamsMessageCard {
return teamsMessageCard{
Type: "MessageCard",
Context: "http://schema.org/extensions",
ThemeColor: severityHexColor(event.Severity),
Summary: event.Title,
Title: event.Title,
Text: event.Body,
}
}
func (d *Dispatcher) deliverTeams(ctx context.Context, target models.DeliveryTarget, event models.Event) error {
return d.postJSON(ctx, target.ChannelName, target.Address, buildTeamsPayload(d.withOriginTitle(event)))
}
// slackMessage uses the attachment form so the severity color renders as a bar.
type slackAttachment struct {
Color string `json:"color"`
Title string `json:"title"`
Text string `json:"text"`
Footer string `json:"footer,omitempty"`
TS int64 `json:"ts,omitempty"`
}
type slackMessage struct {
Text string `json:"text"`
Attachments []slackAttachment `json:"attachments,omitempty"`
}
func buildSlackPayload(event models.Event) slackMessage {
return slackMessage{
Text: eventText(event),
Attachments: []slackAttachment{{
Color: "#" + severityHexColor(event.Severity),
Title: event.Title,
Text: event.Body,
Footer: event.SourceType + ":" + event.SourceID,
TS: event.CreatedAt,
}},
}
}
func (d *Dispatcher) deliverSlack(ctx context.Context, target models.DeliveryTarget, event models.Event) error {
return d.postJSON(ctx, target.ChannelName, target.Address, buildSlackPayload(d.withOriginTitle(event)))
}