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>
This commit is contained in:
+42
-9
@@ -192,10 +192,12 @@ because they cross domain boundaries:
|
||||
`race_name`) remain `text`.
|
||||
- Foreign keys are intra-domain only: `accounts → entitlement_*` /
|
||||
`sanction_*` / `limit_*`; `games → applications` / `invites` /
|
||||
`memberships` (with `ON DELETE CASCADE`); `mail_payloads →
|
||||
mail_deliveries → mail_recipients` / `mail_attempts` /
|
||||
`mail_dead_letters`; `notifications → notification_routes` /
|
||||
`notification_dead_letters`. Cross-domain references
|
||||
`memberships` / `diplomail_messages` (each with
|
||||
`ON DELETE CASCADE`); `mail_payloads → mail_deliveries →
|
||||
mail_recipients` / `mail_attempts` / `mail_dead_letters`;
|
||||
`notifications → notification_routes` / `notification_dead_letters`;
|
||||
`diplomail_messages → diplomail_recipients` /
|
||||
`diplomail_translations`. Cross-domain references
|
||||
(`memberships.user_id`, `games.owner_user_id`, etc.) are kept as
|
||||
opaque `uuid` columns because each domain runs its own cleanup
|
||||
through the in-process cascade described in [§7](#7-in-process-async-patterns). Adding a database
|
||||
@@ -456,12 +458,15 @@ committed; SMTP completion is asynchronous to the auth request.
|
||||
|
||||
Notifications are an in-process pipeline. The closed catalog is
|
||||
defined in `backend/internal/notification/catalog.go` and currently
|
||||
covers 13 kinds: 10 lobby kinds (invite received/revoked, application
|
||||
covers 16 kinds: 10 lobby kinds (invite received/revoked, application
|
||||
submitted/approved/rejected, membership removed/blocked, race name
|
||||
registered/pending/expired) and 3 admin-recipient runtime kinds
|
||||
(image pull failed, container start failed, start config invalid).
|
||||
Per-kind delivery channels (push, email, or both) and the admin-vs-
|
||||
per-user recipient routing live in the same file.
|
||||
registered/pending/expired), 3 admin-recipient runtime kinds (image
|
||||
pull failed, container start failed, start config invalid), 2 game
|
||||
lifecycle kinds (turn ready, game paused), and the
|
||||
`diplomail.message.received` kind that fans diplomatic-mail send
|
||||
events out to the recipient's push stream. Per-kind delivery channels
|
||||
(push, email, or both) and the admin-vs-per-user recipient routing
|
||||
live in the same file.
|
||||
|
||||
For every intent, `notification.Submit` performs:
|
||||
|
||||
@@ -490,6 +495,34 @@ Notification persistence is the auditable record of "we tried to tell
|
||||
this user about this thing"; clients still derive their actual game
|
||||
state through normal user-facing reads.
|
||||
|
||||
### 12.1 Diplomatic mail subsystem
|
||||
|
||||
`backend/internal/diplomail` owns the player-to-player message channel
|
||||
that the in-game mail view consumes. The data lives in three tables:
|
||||
|
||||
- `diplomail_messages` — one canonical row per send. Captures the
|
||||
game name and the sender IP at insert time so audit rendering
|
||||
survives game renames and bulk purges. `kind` is `personal` (a
|
||||
replyable player→player message) or `admin` (a non-replyable
|
||||
notification produced by an administrator or the system).
|
||||
`sender_kind` distinguishes `player`, `admin`, and `system` senders.
|
||||
`broadcast_scope` carries `single`, `game_broadcast`, or
|
||||
`multi_game_broadcast`.
|
||||
- `diplomail_recipients` — one row per (message, recipient). Holds
|
||||
the per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at`
|
||||
state plus snapshot fields (`recipient_user_name`,
|
||||
`recipient_race_name`) so admin search and the inbox listing render
|
||||
correctly even after the source rows are renamed or revoked.
|
||||
- `diplomail_translations` — cached per-language rendering shared
|
||||
across every recipient with the same `accounts.preferred_language`.
|
||||
|
||||
Stage A wires the personal subset (single recipient, no language
|
||||
detection). Lifecycle hooks (paused / cancelled / kicked), paid-tier
|
||||
player broadcasts, multi-game admin broadcasts, bulk purge, and the
|
||||
detection / translation cache land in later stages. The package is
|
||||
the only place that constructs `diplomail.message.received` push
|
||||
intents; the notification pipeline takes it from there.
|
||||
|
||||
## 13. Container Lifecycle (in-process)
|
||||
|
||||
`backend/internal/runtime` owns the lifecycle of game-engine containers
|
||||
|
||||
Reference in New Issue
Block a user