Documentation
¶
Overview ¶
Package notifypost is the transport half of notification channels: one Sender per chat or webhook kind, turning a document into a post on its upstream.
It is separate from notifychannel, which persists the operator's channel records, because the two share nothing: a record is read and written by the admin surface, a post is made by the send worker, and neither needs the other's machinery. What they share is the Channel value, which lives in the domain both depend on.
No sender holds a credential. The three kinds deliver through an Upstream the api gateway materializes for the connection the channel names, so a bot token lives exactly where every other upstream credential lives.
Index ¶
Constants ¶
const MattermostMaxTextBytes = 16000
MattermostMaxTextBytes is the most a mattermost channel sends in one message. A Mattermost post is capped at 16,383 characters by the server; the platform cuts below that and keeps the link, so the server never refuses a post for its length.
const ( // SlackMaxMarkdownBytes is the most a slack channel sends in its // markdown block. The platform cuts below it and keeps the link. SlackMaxMarkdownBytes = 12000 )
Slack's bounds. The markdown block's cap is cumulative across a payload's markdown blocks; the header's is its plain text.
const WebhookMaxTextBytes = 12000
WebhookMaxTextBytes is the most a webhook channel sends in one message. An incoming webhook is consumed by whichever server the URL belongs to, most often Slack or Mattermost, so the cap is the lower of the two.
Variables ¶
var ErrTerminal = errors.New("notifychannel: upstream refused the message")
ErrTerminal marks a send failure that retrying cannot fix: the upstream refused the message itself rather than failing to receive it. A channel posting to a chat channel that was deleted, or through a bot that was uninstalled, fails this way on every attempt, so burning five of them delays nothing and fills the history with the same line five times.
The worker matches it with errors.Is and fails the row at once. Everything else -- a timeout, a refused connection, a 5xx, a rate limit -- retries on the existing backoff.
Functions ¶
func Bulletin ¶ added in v1.143.0
func Bulletin(ch notification.Channel, ds []notification.Delivery) notification.Delivery
Bulletin composes a daily channel's batch into the one document it posts: a line per document with its title, its first paragraph and its link, so a day's findings read as one summary rather than as a burst of messages.
Attachments are not carried. A bulletin is a summary, and each item's link is where its files are; carrying five files from each of a day's documents would make the bulletin the burst it exists to replace.
Types ¶
type DeliveryIDCarrier ¶ added in v1.138.3
type DeliveryIDCarrier interface {
DeliveryIDHeader() string
}
DeliveryIDCarrier is an Upstream that names a header a delivery's id is sent in: an hmac connection's hmac_id_header, which its signature covers. *upstreamcall.Upstream implements it.
type Envelope ¶ added in v1.138.3
type Envelope struct {
// ID is the queue row's id, the same on every retry of that row, so a
// receiver deduplicates on it.
ID string `json:"id"`
// Type is what produced the notification, one of the
// notification.Document* types; a receiver routes on it.
Type string `json:"type"`
OccurredAt time.Time `json:"occurred_at"`
Deployment string `json:"deployment,omitempty"`
Channel string `json:"channel"`
Title string `json:"title"`
// Body is the markdown body as sent, whole: a system reads fields, and a
// chat client's display cap does not apply to it.
Body string `json:"body,omitempty"`
Link string `json:"link,omitempty"`
Source *notification.DocumentSource `json:"source,omitempty"`
// Attachments name the files the notification carries, each with its
// link; a webhook carries no file content.
Attachments []notification.Attachment `json:"attachments,omitempty"`
// Data is the sender's structured payload, verbatim.
Data json.RawMessage `json:"data,omitempty"`
}
Envelope is the body a JSON webhook channel posts for one notification.
type MattermostSender ¶
type MattermostSender struct {
// contains filtered or unexported fields
}
MattermostSender posts to a Mattermost channel through the v4 REST API.
The bot token is the connection's credential, applied by the connection's authenticator as a bearer token, so this sender never sees it.
func NewMattermostSender ¶
func NewMattermostSender(upstream UpstreamFunc) *MattermostSender
NewMattermostSender builds the mattermost transport over upstream.
func (*MattermostSender) Send ¶
func (s *MattermostSender) Send(ctx context.Context, ch notification.Channel, d notification.Delivery) error
Send uploads the delivery's files to the channel, then posts the document with them.
Mattermost answers an application-level refusal with the matching HTTP status rather than with a 200 and a flag, so the shared status classification is the whole verdict and there is no envelope to read.
An upload that fails fails the send, retried or terminal by the same classification as the post: a message that names files it does not carry would read as the report having been sent when it was not. A retry uploads again; an upload is not a post, so a file Mattermost holds from an attempt that went no further is shown to nobody.
type RetryAfterError ¶ added in v1.138.3
RetryAfterError is a send the upstream asked to have retried after a delay: a 429 or a 503 carrying Retry-After. The worker waits that long, within its own longest backoff, rather than its own schedule.
func (*RetryAfterError) Error ¶ added in v1.138.3
func (r *RetryAfterError) Error() string
Error is the upstream's answer.
func (*RetryAfterError) Unwrap ¶ added in v1.138.3
func (r *RetryAfterError) Unwrap() error
Unwrap returns the upstream's answer.
type Sender ¶
type Sender interface {
// Send posts d to ch, returning ErrTerminal for a refusal that
// retrying cannot fix.
Send(ctx context.Context, ch notification.Channel, d notification.Delivery) error
}
Sender delivers one document to one channel of its kind.
type Senders ¶
type Senders struct {
// contains filtered or unexported fields
}
Senders dispatches a document to the sender for its channel's kind.
The email kind is absent: an email channel fans out at enqueue to one ordinary queue row per recipient, which the existing renderer and SMTP sender deliver. There is nothing for a channel transport to do with it, and a second mail path would be a second place for the deployment's mail settings to be read.
func NewSenders ¶
func NewSenders(upstream UpstreamFunc, deployment string) *Senders
NewSenders builds the dispatcher for the HTTP kinds over upstream.
deployment names this deployment in a JSON webhook envelope, so a receiver fed by several can tell them apart.
func (*Senders) Send ¶
func (s *Senders) Send(ctx context.Context, ch notification.Channel, d notification.Delivery) error
Send delivers d through the sender for ch's kind.
type SlackSender ¶ added in v1.143.0
type SlackSender struct {
// contains filtered or unexported fields
}
SlackSender posts to a Slack channel through the Web API with the bot token the connection holds as its bearer credential.
Slack answers an application-level refusal with HTTP 200 and ok: false, so the shared status classification is not the whole verdict: every answer's envelope is read, and its error code decides whether a retry could help.
func NewSlackSender ¶ added in v1.143.0
func NewSlackSender(upstream UpstreamFunc) *SlackSender
NewSlackSender builds the slack transport over upstream.
func (*SlackSender) Send ¶ added in v1.143.0
func (s *SlackSender) Send(ctx context.Context, ch notification.Channel, d notification.Delivery) error
Send uploads the delivery's files, posts the document, then shares the files into the post's thread.
The upload's first two steps come before the post: a file Slack has received but nobody has shared is seen by nobody, so a failure there costs a retry nothing. The share comes after, so the files sit with the message they belong to. A share that fails after the post is terminal and says the message was posted: retrying would post the message a second time.
type Upstream ¶
type Upstream interface {
// BaseURL is the connection's upstream root.
BaseURL() string
// Do applies the connection's authentication and static headers and
// sends the request.
Do(req *http.Request) (*http.Response, error)
}
Upstream is the authorized transport a channel delivers through: the connection's base URL and its ability to make an authorized call.
It is an interface here, satisfied by *apigateway.Upstream, so this package depends on the idea of an authorized upstream rather than on the api gateway. That keeps the senders testable against an httptest server and keeps the credential on the gateway's side of the seam.
type UpstreamFunc ¶
UpstreamFunc resolves a connection name to its authorized transport. The platform wires it to the api gateway; a test wires it to an httptest server.
type WebhookSender ¶
type WebhookSender struct {
// contains filtered or unexported fields
}
WebhookSender posts to a webhook in the channel's format: one {"text": ...} body for a chat client's incoming webhook (the default), or one JSON envelope for a system receiving events (#1997).
The whole address is the connection's base_url, including the secret path segment an incoming webhook URL carries, so the kind sends to the base itself and names no path. That secret is the connection's to hold: a webhook URL is a bearer credential written as a URL, which is why the kind names a connection like the two token kinds rather than storing a URL of its own. A connection can hold the segment as path_secret, encrypted, rather than in its base_url.
A JSON delivery is signed when the connection's auth_mode is hmac: the channel holds no secret and no signing settings of its own.
func NewWebhookSender ¶
func NewWebhookSender(upstream UpstreamFunc, deployment string) *WebhookSender
NewWebhookSender builds the webhook transport over upstream.
func (*WebhookSender) Send ¶
func (s *WebhookSender) Send(ctx context.Context, ch notification.Channel, d notification.Delivery) error
Send posts the delivery to the webhook.