Documentation
¶
Overview ¶
Package notifications owns durable mail delivery records and announcement baselines.
Index ¶
- Constants
- Variables
- func RegisterRoutes(e *echo.Echo, module UseCases, diagnostic Diagnostic, ...)
- type Approval
- type ApprovePerson
- type ApproveRequest
- type Baseline
- type Delivery
- type DeliveryError
- type DeliveryWork
- type Diagnostic
- type DismissRequest
- type EnqueueDelivery
- type MailStatus
- type Mailer
- type Message
- type Module
- func (m *Module) Announced(ctx context.Context, db bun.IDB, personID string) (Baseline, error)
- func (m *Module) AnnouncedEntryIDs(ctx context.Context, personID string) ([]string, error)
- func (m *Module) ApproveUpdates(ctx context.Context, request ApproveRequest) (Approval, error)
- func (m *Module) CheckMail(ctx context.Context) MailStatus
- func (m *Module) Configured() bool
- func (m *Module) Deliveries(ctx context.Context, db bun.IDB, ids []string) (map[string]Delivery, error)
- func (m *Module) DeliveryStates(ctx context.Context, ids []string) (map[string]Delivery, error)
- func (m *Module) DismissUpdates(ctx context.Context, request DismissRequest) (Approval, error)
- func (m *Module) Enqueue(ctx context.Context, tx bun.Tx, message Message) (Delivery, error)
- func (m *Module) Execute(ctx context.Context, deliveryID string, final bool) error
- func (m *Module) ListNotifications(ctx context.Context, personID string) (NotificationList, error)
- func (m *Module) MarkAllRead(ctx context.Context, personID string) (NotificationList, error)
- func (m *Module) MarkRead(ctx context.Context, personID, notificationID string) (Notification, error)
- func (m *Module) OpenDeliveries(ctx context.Context) ([]DeliveryWork, error)
- func (m *Module) PreviewUpdates(ctx context.Context) (Preview, error)
- func (m *Module) RecordBaseline(ctx context.Context, tx bun.Tx, personID string) (Baseline, error)
- func (m *Module) RecoverInterrupted(ctx context.Context) (int, error)
- func (m *Module) Retry(ctx context.Context, tx bun.Tx, deliveryID string) (Delivery, error)
- func (m *Module) RetryDelivery(ctx context.Context, deliveryID string) (Delivery, error)
- func (m *Module) Unsubscribe(ctx context.Context, token string) (UnsubscribeStatus, error)
- func (m *Module) UnsubscribeStatus(ctx context.Context, token string) (UnsubscribeStatus, error)
- type Notification
- type NotificationAlbum
- type NotificationList
- type Outcome
- type PersonResult
- type Preview
- type PreviewPerson
- type Recorder
- type SMTPMailer
- type UnsubscribeStatus
- type UseCases
- type VisibleContent
- type VisibleEntry
Constants ¶
const ( StatusQueued = "queued" StatusSending = "sending" StatusDelivered = "delivered" StatusFailed = "failed" StatusUncertain = "uncertain" // StatusSkipped means send-time validation found no eligible recipient // or content, so nothing was sent and nothing needs a Curator. StatusSkipped = "skipped" )
const ( AlbumNew = "new" AlbumUpdated = "updated" ResultNotified = "notified" ResultDismissed = "dismissed" ResultSkipped = "skipped" )
const KindUpdate = "update"
KindUpdate marks the optional email for an approved Update Notification. Unlike transactional kinds it is re-validated and re-rendered when sent.
Variables ¶
var ( ErrMailUnconfigured = &errcodes.Error{HTTPCode: 409, Code: "mail_unconfigured", Message: "Email is not configured for this installation. Add SMTP settings to send email."} ErrAlreadyDelivered = &errcodes.Error{HTTPCode: 409, Code: "already_delivered", Message: "This email was already delivered."} )
var ErrInvitationRetry = &errcodes.Error{HTTPCode: 409, Code: "invitation_retry", Message: "Retry this invitation from the person's page."}
Functions ¶
func RegisterRoutes ¶
func RegisterRoutes(e *echo.Echo, module UseCases, diagnostic Diagnostic, requirePerson, requireCurator echo.MiddlewareFunc)
RegisterRoutes keeps previews, approval, delivery state, and the email diagnostic behind the Curator guard and a Person's own notifications behind the signed-in guard. The unsubscribe link is public by design: it is opened from an email without a session, and only its POST changes anything. Its token is a query value so the access log, which records paths, never carries it.
Types ¶
type Approval ¶
type Approval struct {
People []PersonResult `json:"people"`
}
Approval reports the outcome of sending or dismissing reviewed updates.
type ApprovePerson ¶
type ApprovePerson struct {
PersonID string `json:"person_id" validate:"required,uuid"`
ReviewToken string `json:"review_token" validate:"required"`
ExcludedAlbumIDs []string `json:"excluded_album_ids" validate:"dive,uuid"`
}
ApprovePerson names one reviewed row. ExcludedAlbumIDs drops whole Album updates from that row; a Person the Curator left out is simply omitted.
type ApproveRequest ¶
type ApproveRequest struct {
Note string `json:"note" mod:"trim" validate:"max=1000"`
People []ApprovePerson `json:"people" validate:"required,min=1,dive"`
}
func (ApproveRequest) ValidationMessage ¶
func (ApproveRequest) ValidationMessage(field, rule string) string
ValidationMessage keeps approval failures in Curator words. A stale or malformed row means the preview must be reviewed again, never edited by hand.
type Delivery ¶
type Delivery struct {
ID string `json:"id"`
Status string `json:"status"`
Attempts int `json:"attempts"`
Message string `json:"message"`
UpdatedAt time.Time `json:"updated_at"`
DeliveredAt *time.Time `json:"delivered_at"`
}
Delivery is the Curator-facing state of one email. Status is one of queued, sending, delivered, failed, uncertain, or skipped.
type DeliveryError ¶
DeliveryError carries the outcome and a short operator-safe summary. The cause keeps provider details for logs without exposing them to Curators.
func (*DeliveryError) Error ¶
func (e *DeliveryError) Error() string
func (*DeliveryError) Unwrap ¶
func (e *DeliveryError) Unwrap() error
type DeliveryWork ¶
type DeliveryWork struct {
Delivery `tstype:",extends"`
Kind string `json:"kind"`
Recipient string `json:"recipient"`
PersonID string `json:"person_id"`
PersonName string `json:"person_name"`
InvitationID string `json:"invitation_id"`
}
DeliveryWork is one email a Curator may need to watch or act on. PersonID and PersonName are set for update and Invitation email; InvitationID is set for Invitations, whose retry lives on the Person page.
type Diagnostic ¶ added in v0.2.0
type Diagnostic interface {
CheckMail(context.Context) MailStatus
}
Diagnostic is the Settings seam: one read-only check of the mail server.
type DismissRequest ¶
type DismissRequest struct {
People []ApprovePerson `json:"people" validate:"required,min=1,dive"`
}
DismissRequest selects reviewed changes to add to the baseline silently.
func (DismissRequest) ValidationMessage ¶
func (DismissRequest) ValidationMessage(field, rule string) string
type EnqueueDelivery ¶
EnqueueDelivery records durable work in the caller's transaction. It must not send.
type MailStatus ¶ added in v0.2.0
type MailStatus struct {
Configured bool `json:"configured"`
Usable bool `json:"usable"`
Sender string `json:"sender"`
Message string `json:"message"`
}
MailStatus is the Settings diagnostic for email. Configured is false when the installation has no SMTP settings, which is a quiet state rather than a failure. Usable is true once the mail server accepted a connection and any sign-in. Sender is the configured From address; the server address and credentials are never reported.
type Mailer ¶
type Mailer interface {
Send(ctx context.Context, message Message) error
Check(ctx context.Context) MailStatus
}
Mailer is the adapter seam for SMTP and its recording test counterpart. Send returns nil when the server accepted the message and a *DeliveryError otherwise. Check connects and signs in without sending anything.
type Module ¶
type Module struct {
// PublicURL is the origin update emails link to. Empty renders relative links.
PublicURL string
// contains filtered or unexported fields
}
Module owns delivery records, announcement baselines, and Update Notifications. A nil mailer means SMTP is not configured; every other use case still works.
func New ¶
func New(db *bun.DB, mailer Mailer, enqueue EnqueueDelivery, content VisibleContent, now func() time.Time) *Module
func (*Module) Announced ¶
Announced counts the Person's baseline. Callers use it to show or assert that later visibility changes stay unannounced.
func (*Module) AnnouncedEntryIDs ¶
AnnouncedEntryIDs lists announced Album Entries for exact assertions.
func (*Module) ApproveUpdates ¶
ApproveUpdates creates one immutable notification per reviewed Person and records exactly its Album Entries as announced, all in one transaction.
func (*Module) CheckMail ¶ added in v0.2.0
func (m *Module) CheckMail(ctx context.Context) MailStatus
CheckMail is the Settings diagnostic: it connects to the configured mail server without sending anything. Without SMTP it reports the quiet unconfigured state instead of a failure.
func (*Module) Configured ¶
Configured reports whether email can be sent from this installation.
func (*Module) Deliveries ¶
func (m *Module) Deliveries(ctx context.Context, db bun.IDB, ids []string) (map[string]Delivery, error)
Deliveries loads Curator-facing state for the given IDs in one query.
func (*Module) DeliveryStates ¶
DeliveryStates loads Curator-facing state for the given IDs, for pages that watch a batch settle.
func (*Module) DismissUpdates ¶
DismissUpdates advances the baseline for the selected, reviewed changes without creating a notification or email. Excluded Albums stay pending.
func (*Module) Enqueue ¶
Enqueue stores the complete message and commits its durable work with the caller's transaction. Preferences are the caller's concern: transactional mail such as Invitations never consults the update-email setting.
func (*Module) Execute ¶
Execute is the worker body for one delivery. It claims the record, sends outside any transaction, then records the outcome. A record still marked sending when a new attempt starts means an earlier process died mid-session, so the delivery becomes uncertain instead of being sent again. Update email is re-validated while claiming: an ineligible recipient or fully revoked content skips the send without touching the in-app notification.
func (*Module) ListNotifications ¶
ListNotifications returns the Person's own notifications, newest first.
func (*Module) MarkAllRead ¶
MarkAllRead is the bulk action; already-read rows keep their original time.
func (*Module) MarkRead ¶
func (m *Module) MarkRead(ctx context.Context, personID, notificationID string) (Notification, error)
MarkRead sets the read time once. Reading is independent from browsing and from announcement state, and never changes the stored summary.
func (*Module) OpenDeliveries ¶
func (m *Module) OpenDeliveries(ctx context.Context) ([]DeliveryWork, error)
OpenDeliveries lists every email that has not settled: queued and sending work still running, and failed or uncertain work waiting for a Curator. Invitations are read here too so one list covers the dashboard; their retry stays with Identity.
func (*Module) PreviewUpdates ¶
PreviewUpdates lists every eligible Person with unannounced content. It changes nothing; approval freezes what each row showed through its token.
func (*Module) RecordBaseline ¶
RecordBaseline marks everything the Person can currently view as announced, inside the caller's transaction so it commits with the owning transition. Existing associations are kept, so repeating the call never announces more.
func (*Module) RecoverInterrupted ¶
RecoverInterrupted runs once at startup. A delivery still marked sending belonged to a process that died mid-session, so its outcome is unknown and only a Curator may send it again. River's rescued job later finds nothing to do. This assumes Memento's single-process deployment: a second process starting while the first is mid-send would mark that send uncertain too.
func (*Module) Retry ¶
Retry requeues a failed, uncertain, or skipped delivery. Repeated clicks and concurrent submissions are idempotent: an already queued or sending delivery is returned unchanged.
func (*Module) RetryDelivery ¶
RetryDelivery is the Curator's deliberate resend for a failed or uncertain update or alert email. Invitations keep their own retry, which also checks that the Person may still be invited.
func (*Module) Unsubscribe ¶
Unsubscribe switches off update email only. In-app notifications, Invitations, and account-security email are unaffected, and the selected destination stays so the Person can opt back in from their profile.
func (*Module) UnsubscribeStatus ¶
UnsubscribeStatus describes the link's owner without changing anything, so a mail scanner that loads the link cannot unsubscribe anyone.
type Notification ¶
type Notification struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
ReadAt *time.Time `json:"read_at"`
Albums []NotificationAlbum `json:"albums"`
Note string `json:"note"`
}
Notification is a Person's own approved summary. It renders from stored JSON, so it survives title edits and Album deletion; the Album list and pages still decide what the Person may open today.
type NotificationAlbum ¶
type NotificationAlbum struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
PhotoCount int `json:"photo_count"`
VideoCount int `json:"video_count"`
}
NotificationAlbum is one Album inside an approved summary. Status is new when the Album had never been announced to the Person, otherwise updated. Photos and videos are only counted, never listed.
type NotificationList ¶
type NotificationList struct {
Notifications []Notification `json:"notifications"`
Unread int `json:"unread"`
}
type Outcome ¶
type Outcome string
Outcome classifies a failed send so the module can choose between automatic retry, a final failure, and a deliberate Curator decision.
const ( // OutcomeTransient means the server did not accept the message and a later attempt is safe. OutcomeTransient Outcome = "transient" // OutcomePermanent means the server rejected the message and retrying the same message is pointless. OutcomePermanent Outcome = "permanent" // OutcomeUncertain means the message may have been accepted; only a Curator may resend it. OutcomeUncertain Outcome = "uncertain" )
type PersonResult ¶
type PersonResult struct {
PersonID string `json:"person_id"`
DisplayName string `json:"display_name"`
Status string `json:"status"`
Message string `json:"message"`
NotificationID string `json:"notification_id"`
AlbumCount int `json:"album_count"`
PhotoCount int `json:"photo_count"`
VideoCount int `json:"video_count"`
Email string `json:"email"`
Delivery *Delivery `json:"delivery"`
}
PersonResult reports one review outcome. Status is notified, dismissed, or skipped; Message explains a skip in Curator-facing words. Email and Delivery are set only when an email was queued.
type Preview ¶
type Preview struct {
People []PreviewPerson `json:"people"`
EmailConfigured bool `json:"email_configured"`
}
Preview is the batch a Curator reviews. EmailConfigured is false when the installation has no SMTP settings, so every row is in app only.
type PreviewPerson ¶
type PreviewPerson struct {
PersonID string `json:"person_id"`
DisplayName string `json:"display_name"`
UpdateEmail string `json:"update_email"`
EmailUpdates bool `json:"email_updates"`
EmailEligible bool `json:"email_eligible"`
Albums []NotificationAlbum `json:"albums"`
ReviewToken string `json:"review_token"`
}
PreviewPerson is one collapsible row. EmailEligible reports whether a later email delivery could go anywhere; the in-app notification is created either way. ReviewToken freezes what the row showed for approval.
type Recorder ¶
type Recorder struct {
// BeforeSend runs before the message is recorded. A returned error is the send result.
BeforeSend func(context.Context, Message) error
// AfterSend runs after the message is recorded. A returned error is the send result.
AfterSend func(context.Context, Message) error
// contains filtered or unexported fields
}
Recorder is the deterministic Mailer for tests and fixtures. Hooks run before and after the simulated acceptance so tests can pause, fail, or cancel at the same points where a real SMTP session can break.
type SMTPMailer ¶
type SMTPMailer struct {
// contains filtered or unexported fields
}
SMTPMailer delivers through one configured server. smtp:// connects in plain text and upgrades with STARTTLS when the server offers it; smtps:// uses TLS from the first byte.
func NewSMTPMailer ¶
func NewSMTPMailer(rawURL, from string) (*SMTPMailer, error)
NewSMTPMailer validates the configured URL without connecting. Errors name the setting, never the URL, because it may contain credentials.
func (*SMTPMailer) Check ¶ added in v0.2.0
func (m *SMTPMailer) Check(ctx context.Context) MailStatus
Check connects and signs in without sending anything, so Settings can show whether email would work. The result names the sender, never the server or its credentials.
type UnsubscribeStatus ¶
type UnsubscribeStatus struct {
DisplayName string `json:"display_name"`
Email string `json:"email"`
Subscribed bool `json:"subscribed"`
}
UnsubscribeStatus describes an unsubscribe link's owner. Subscribed is false once update email is off, or when no destination is selected.
type UseCases ¶
type UseCases interface {
PreviewUpdates(context.Context) (Preview, error)
ApproveUpdates(context.Context, ApproveRequest) (Approval, error)
DismissUpdates(context.Context, DismissRequest) (Approval, error)
ListNotifications(ctx context.Context, personID string) (NotificationList, error)
MarkRead(ctx context.Context, personID, notificationID string) (Notification, error)
MarkAllRead(ctx context.Context, personID string) (NotificationList, error)
DeliveryStates(ctx context.Context, ids []string) (map[string]Delivery, error)
RetryDelivery(ctx context.Context, deliveryID string) (Delivery, error)
UnsubscribeStatus(ctx context.Context, token string) (UnsubscribeStatus, error)
Unsubscribe(ctx context.Context, token string) (UnsubscribeStatus, error)
}
UseCases is the HTTP seam. Handlers translate requests; the module owns every transaction and the announcement invariants.
type VisibleContent ¶
type VisibleContent interface {
VisibleEntries(ctx context.Context, db bun.IDB, personID string) ([]VisibleEntry, error)
}
VisibleContent is the consumer-owned view of Publishing that baselines need. It must apply viewer eligibility only, never a Curator's administrative bypass.