Files
Ilia Denisov 535e27008f
Tests · Go / test (push) Successful in 1m44s
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · Go / test (pull_request) Successful in 2m45s
diplomail (Stage A): add in-game personal mail subsystem
Phase 28 of ui/PLAN.md needs a persistent player-to-player mail
channel; the existing `mail` package is a transactional email
outbox and the `notification` catalog is one-way platform events.
Stage A lands the schema (diplomail_messages / _recipients /
_translations), a single-recipient personal send/read/delete
service path, a `diplomail.message.received` push kind plumbed
through the notification pipeline, and an unread-counts endpoint
that drives the lobby badge. Admin / system mail, lifecycle hooks,
paid-tier broadcast, multi-game broadcast, bulk purge and language
detection / translation cache come in stages B–D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:28:55 +02:00

143 lines
4.8 KiB
Go

package notification
// Kind constants name every supported notification kind. The implementation // trims the README §10 catalog to the set with active producers in
// the codebase; further kinds (`game.*`, `mail.dead_lettered`) require
// an additive change here together with a producer.
const (
KindLobbyInviteReceived = "lobby.invite.received"
KindLobbyInviteRevoked = "lobby.invite.revoked"
KindLobbyApplicationSubmitted = "lobby.application.submitted"
KindLobbyApplicationApproved = "lobby.application.approved"
KindLobbyApplicationRejected = "lobby.application.rejected"
KindLobbyMembershipRemoved = "lobby.membership.removed"
KindLobbyMembershipBlocked = "lobby.membership.blocked"
KindLobbyRaceNameRegistered = "lobby.race_name.registered"
KindLobbyRaceNamePending = "lobby.race_name.pending"
KindLobbyRaceNameExpired = "lobby.race_name.expired"
KindRuntimeImagePullFailed = "runtime.image_pull_failed"
KindRuntimeContainerStartFailed = "runtime.container_start_failed"
KindRuntimeStartConfigInvalid = "runtime.start_config_invalid"
KindGameTurnReady = "game.turn.ready"
KindGamePaused = "game.paused"
KindDiplomailReceived = "diplomail.message.received"
)
// CatalogEntry describes the per-kind delivery policy: which channels
// fan out and whether the kind targets the platform admin recipient
// instead of per-user accounts.
type CatalogEntry struct {
// Channels lists the channels this kind fans out to, in the order
// rows are materialised in `notification_routes`. The closed set is
// {`push`, `email`}.
Channels []string
// Admin reports whether the email channel targets the configured
// admin recipient (`BACKEND_NOTIFICATION_ADMIN_EMAIL`) rather than
// per-user accounts. Admin-targeted kinds carry an empty Recipients
// slice on the producer side.
Admin bool
// MailTemplateID is the template_id passed to `mail.EnqueueTemplate`
// for email routes. The catalog uses the kind itself by convention,
// matching `mail.TemplateLoginCode`'s use of `auth.login_code`.
MailTemplateID string
}
// catalog maps each supported kind to its delivery policy. The map is
// queried by Submit and by the dispatcher worker; producers do not
// inspect it directly.
var catalog = map[string]CatalogEntry{
KindLobbyInviteReceived: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyInviteReceived,
},
KindLobbyInviteRevoked: {
Channels: []string{ChannelPush},
},
KindLobbyApplicationSubmitted: {
Channels: []string{ChannelPush},
},
KindLobbyApplicationApproved: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyApplicationApproved,
},
KindLobbyApplicationRejected: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyApplicationRejected,
},
KindLobbyMembershipRemoved: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyMembershipRemoved,
},
KindLobbyMembershipBlocked: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyMembershipBlocked,
},
KindLobbyRaceNameRegistered: {
Channels: []string{ChannelPush},
},
KindLobbyRaceNamePending: {
Channels: []string{ChannelPush, ChannelEmail},
MailTemplateID: KindLobbyRaceNamePending,
},
KindLobbyRaceNameExpired: {
Channels: []string{ChannelPush},
},
KindRuntimeImagePullFailed: {
Channels: []string{ChannelEmail},
Admin: true,
MailTemplateID: KindRuntimeImagePullFailed,
},
KindRuntimeContainerStartFailed: {
Channels: []string{ChannelEmail},
Admin: true,
MailTemplateID: KindRuntimeContainerStartFailed,
},
KindRuntimeStartConfigInvalid: {
Channels: []string{ChannelEmail},
Admin: true,
MailTemplateID: KindRuntimeStartConfigInvalid,
},
KindGameTurnReady: {
Channels: []string{ChannelPush},
},
KindGamePaused: {
Channels: []string{ChannelPush},
},
KindDiplomailReceived: {
Channels: []string{ChannelPush},
},
}
// LookupCatalog returns the per-kind policy and a boolean reporting
// whether the kind exists. Callers (Submit, Worker) branch on the
// boolean rather than receiving a sentinel error.
func LookupCatalog(kind string) (CatalogEntry, bool) {
entry, ok := catalog[kind]
return entry, ok
}
// SupportedKinds returns the closed kind set in deterministic order.
// The function exists to back tests and the migration CHECK constraint
// audit; it is not on the hot path.
func SupportedKinds() []string {
return []string{
KindLobbyInviteReceived,
KindLobbyInviteRevoked,
KindLobbyApplicationSubmitted,
KindLobbyApplicationApproved,
KindLobbyApplicationRejected,
KindLobbyMembershipRemoved,
KindLobbyMembershipBlocked,
KindLobbyRaceNameRegistered,
KindLobbyRaceNamePending,
KindLobbyRaceNameExpired,
KindRuntimeImagePullFailed,
KindRuntimeContainerStartFailed,
KindRuntimeStartConfigInvalid,
KindGameTurnReady,
KindGamePaused,
KindDiplomailReceived,
}
}