// Package notifications is the one writer of the per-user notifications inbox // (M489, #726). Producers across the server call Notifier.Notify; nothing else // inserts into user_notifications. // // The event bus alone was fire-and-forget: a client that was not connected // when a request completed, or when tracks went missing, never heard of it. A // row here is the durable record. The bus only nudges open clients to come // and read it. package notifications // Kind names one sort of notification. The set is CHECK-gated in migration // 0073, swapped since by 0075 (rule 36): a new kind adds its value in a // migration of the same change, and TestEveryKindPassesTheSchemaChecks // fails until it does. type Kind string const ( KindRequestApproved Kind = "request_approved" KindRequestRejected Kind = "request_rejected" KindRequestCompleted Kind = "request_completed" KindRequestPending Kind = "request_pending" KindQuarantineFlagged Kind = "quarantine_flagged" KindScanFailed Kind = "scan_failed" KindTracksMissing Kind = "tracks_missing" KindDuplicatesFound Kind = "duplicates_found" // KindDuplicatesResolved: the duplicate resolver (M498) merged copies or // changed a Lidarr release on its own. The operator asked for that to // happen automatically; this is how it stays visible. KindDuplicatesResolved Kind = "duplicates_resolved" KindPlaybackErrors Kind = "playback_errors" ) // Audience is who a kind can reach. Admin kinds are never offered to, or // stored for, a non-admin. type Audience int const ( AudienceRequester Audience = iota AudienceAdmin ) // EmailGroup is how a kind's emails are grouped. Nothing is emailed per event. type EmailGroup int const ( // EmailBatch: one email per batch window, holding everything that // accumulated since the first un-emailed item. EmailBatch EmailGroup = iota // EmailSummary: at most one summary a day, at a set local hour. For new // music arriving, which comes in bursts and is not urgent. EmailSummary ) // Channels are where a kind reaches a user. Inbox is the master switch: with // it off nothing is stored, so there is nothing for the phone or email to // deliver. Effective applies that. type Channels struct { Inbox bool Phone bool Email bool } // Effective is what will actually be delivered: phone and email ride on the // inbox row, so they cannot be on without it. func (c Channels) Effective() Channels { return Channels{Inbox: c.Inbox, Phone: c.Inbox && c.Phone, Email: c.Inbox && c.Email} } type spec struct { audience Audience // coalesce: while one of these is unread, a new event updates it in place // rather than adding a row. For the burst-prone admin kinds. coalesce bool // sumCount: a coalesced update adds the payload's `count` to the unread // row's, because each event is "N more". Without it the newer payload // replaces the older, because each event states the whole current total. sumCount bool group EmailGroup defaults Channels } var ( allOn = Channels{Inbox: true, Phone: true, Email: true} healthAlert = Channels{Inbox: true, Phone: true, Email: false} // inboxOnly: news an admin may want to read but never needs to be // interrupted for. inboxOnly = Channels{Inbox: true} ) var specs = map[Kind]spec{ KindRequestApproved: {audience: AudienceRequester, group: EmailBatch, defaults: allOn}, KindRequestRejected: {audience: AudienceRequester, group: EmailBatch, defaults: allOn}, KindRequestCompleted: {audience: AudienceRequester, group: EmailSummary, defaults: allOn}, KindRequestPending: {audience: AudienceAdmin, group: EmailBatch, defaults: allOn}, KindQuarantineFlagged: {audience: AudienceAdmin, group: EmailBatch, defaults: allOn}, KindScanFailed: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: healthAlert}, KindTracksMissing: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: healthAlert}, KindDuplicatesFound: {audience: AudienceAdmin, coalesce: true, group: EmailBatch, defaults: healthAlert}, // Each pass reports what it did itself, so the counts add up. KindDuplicatesResolved: {audience: AudienceAdmin, coalesce: true, sumCount: true, group: EmailBatch, defaults: inboxOnly}, KindPlaybackErrors: {audience: AudienceAdmin, coalesce: true, group: EmailBatch, defaults: healthAlert}, } // order is the display order for settings screens: the requester's own kinds // first, then the admin ones. var order = []Kind{ KindRequestApproved, KindRequestRejected, KindRequestCompleted, KindRequestPending, KindQuarantineFlagged, KindScanFailed, KindTracksMissing, KindDuplicatesFound, KindDuplicatesResolved, KindPlaybackErrors, } // Kinds returns every kind in display order. func Kinds() []Kind { return append([]Kind(nil), order...) } // Valid reports whether k is a known kind. func (k Kind) Valid() bool { _, ok := specs[k]; return ok } // AdminOnly reports whether k reaches only admins. func (k Kind) AdminOnly() bool { return specs[k].audience == AudienceAdmin } // Defaults are the channels a user has for k until they change them. func (k Kind) Defaults() Channels { return specs[k].defaults } // EmailGroup is how k's emails are grouped. func (k Kind) EmailGroup() EmailGroup { return specs[k].group } // coalesceKey is the key k's rows coalesce on, or "" when k never coalesces. // One key per kind: two unread "tracks missing" rows would only split a count. func (k Kind) coalesceKey() string { if specs[k].coalesce { return string(k) } return "" }