// Package notify is how Terdut reaches a person: a Provider per way of doing it // (ntfy today), and a Registry of the ones this server has been configured with. // // The package knows nothing about incidents. The notifier renders a Message from // an incident and hands it to whichever Provider the recipient's channel names, // so a new way of paging someone is one new Provider and nothing else. package notify import ( "context" "sort" ) // Message kinds. Providers may use them to shape a message (ntfy sends a // "resolved" quietly), but a Provider that ignores them is correct. const ( KindTriggered = "triggered" KindReminder = "reminder" KindEscalated = "escalated" KindResolved = "resolved" KindTest = "test" ) // Message is one notification, in no provider's format. type Message struct { // Address is where this provider delivers it: an ntfy topic, later an email // address. Opaque to everything except the Provider that owns it. Address string Kind string Title string Body string // Level is the incident's severity on a 0-4 scale: 0 unknown, 1 info, // 2 warning, 3 error, 4 critical. A provider with a notion of urgency maps it. Level int // ClickURL opens the incident in the web UI. Empty when the server does not // know its own public URL. ClickURL string // AckURL acknowledges the incident when POSTed. Only set for a provider whose // Capabilities say it can render an action; for the rest the notifier puts // the link in Body instead. AckURL string } // Capabilities says what a Provider can render, so the notifier does not have to // know which provider it is talking to. type Capabilities struct { // ActionButtons is whether the provider can show a button that POSTs to a URL // without opening anything, which is what the Acknowledge button is. ActionButtons bool } // Provider is one way of delivering a Message. type Provider interface { // Type is the stable identifier stored in a channel row and shown in the // API: "ntfy". Lower case, never changes once released. Type() string // Label is the name a person sees: "ntfy". Label() string // AddressLabel and AddressHint describe the address field for the UI: a // "Topic" labelled field with a sentence on what to put in it. AddressLabel() string AddressHint() string Capabilities() Capabilities // Validate checks an address a user typed and returns it normalised. An error // is shown to that user as is. Validate(address string) (string, error) // Send delivers one message. It must honour ctx, and an error means the // message did not land, so the notifier will retry. Send(ctx context.Context, msg Message) error } // Target names a provider and an address without belonging to a user: the // fallback for an incident nobody is on call for. type Target struct { Type string `json:"type"` Address string `json:"address"` } // IsZero reports whether no target is set. func (t Target) IsZero() bool { return t.Type == "" && t.Address == "" } // Registry is the providers a server was configured with. The zero value and // nil are both an empty registry, so a server with nothing configured needs no // special case at its call sites. type Registry struct { byType map[string]Provider } // NewRegistry registers providers by Type. A later provider of the same type // replaces the earlier one. func NewRegistry(providers ...Provider) *Registry { r := &Registry{byType: make(map[string]Provider, len(providers))} for _, p := range providers { r.byType[p.Type()] = p } return r } // Get returns the provider for a type. func (r *Registry) Get(typ string) (Provider, bool) { if r == nil { return nil, false } p, ok := r.byType[typ] return p, ok } // Has reports whether a provider of this type is configured. func (r *Registry) Has(typ string) bool { _, ok := r.Get(typ) return ok } // Empty reports whether no provider is configured, which disables notifications. func (r *Registry) Empty() bool { return r == nil || len(r.byType) == 0 } // List returns the providers ordered by type, so the UI and the tests see them // in a stable order. func (r *Registry) List() []Provider { if r == nil { return nil } out := make([]Provider, 0, len(r.byType)) for _, p := range r.byType { out = append(out, p) } sort.Slice(out, func(i, j int) bool { return out[i].Type() < out[j].Type() }) return out }