127 lines
3.6 KiB
Go
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)))
|
|
}
|