Files
galaxy-game/backend/internal/diplomail/README.md
T
Ilia Denisov b3f24cc440
Tests · Go / test (push) Successful in 1m52s
Tests · Go / test (pull_request) Successful in 1m53s
Tests · Integration / integration (pull_request) Successful in 1m36s
diplomail (Stage B): admin/owner sends + lifecycle hooks
Item 7 of the spec wants game-state and membership-state changes to
land as durable inbox entries the affected players can re-read after
the fact — push alone times out of the 5-minute ring buffer. Stage B
adds the admin-kind send matrix (owner-driven via /user, site-admin
driven via /admin) plus the lobby lifecycle hooks: paused / cancelled
emit a broadcast system mail to active members, kick / ban emit a
single-recipient system mail to the affected user (which they keep
read access to even after the membership row is revoked, per item 8).

Migration relaxes diplomail_messages_kind_sender_chk so an owner
sending kind=admin keeps sender_kind=player; the new
LifecyclePublisher dep on lobby.Service is wired through a thin
adapter in cmd/backend/main, mirroring how lobby's notification
publisher is plumbed today.

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

4.7 KiB

diplomail

diplomail owns the diplomatic-mail subsystem of the Galaxy backend service. Messages live in the lobby-side domain (their storage and lifecycle are tied to a game), but they are surfaced inside the game UI — the lobby exposes only an unread-count badge per game.

Stages

The package ships in four staged increments. Stage A is the surface described below; the remaining stages add admin / system mail, lifecycle hooks, paid-tier broadcast, multi-game broadcast, bulk purge, and the language-detection / translation cache.

Stage Scope Status
A Schema, personal single-recipient send / read / delete, unread badge, push event with body-language und shipped
B Owner / admin sends + lifecycle hooks (paused, cancelled, kick); strict soft-access for kicked players shipped
C Paid-tier personal broadcast + admin multi-game broadcast + bulk purge planned
D Body-language detection (whatlanggo) + translation cache + async worker planned

Tables

Three Postgres tables in the backend schema:

  • diplomail_messages — one row per send (personal, admin, or system). Captures game_name and IP at insert time so audit rendering survives renames and purges.
  • diplomail_recipients — one row per (message, recipient). Holds per-user read_at, deleted_at, delivered_at, notified_at state. Snapshot fields (recipient_user_name, recipient_race_name) are captured at insert time and survive membership revocation.
  • diplomail_translations — cached per (message, target_lang) rendering. One translation is reused across every recipient that asks for that language.

Permissions

Action Caller Pre-conditions
Send personal user active membership in game; recipient is active member
Send admin (single user) game owner OR site admin recipient is any-status member of the game
Send admin (broadcast) game owner OR site admin recipient scope ∈ active / active_and_removed / all_members; sender excluded
Read message the recipient row exists in diplomail_recipients(message_id, user_id); non-active members see admin-kind only
Mark read the recipient row exists; idempotent if already marked
Soft delete the recipient read_at IS NOT NULL (open-then-delete, item 10)

Stage C will add the paid-tier player broadcast and the bulk-purge admin endpoint.

System mail is produced internally by lobby lifecycle hooks: Service.transition() emits game.paused / game.cancelled system mail to every active member; Service.changeMembershipStatus / Service.AdminBanMember emit membership.removed / membership.blocked system mail addressed to the affected user.

Content rules

  • Body is plain UTF-8 text. The server does not parse, sanitise, or escape HTML — the UI renders messages via textContent.
  • Body length is capped by BACKEND_DIPLOMAIL_MAX_BODY_BYTES (default 4096). Subject length is capped by BACKEND_DIPLOMAIL_MAX_SUBJECT_BYTES (default 256). Both limits live in the service layer so they can be tuned without a schema migration.
  • body_lang is stored as the BCP 47 und (undetermined) sentinel until Stage D wires the auto-detector.

Push integration

Every successful send emits a diplomail.message.received push intent through the existing notification pipeline. The catalog entry limits delivery to the push channel — email is intentionally absent; the inbox endpoint is the durable fallback for offline users. The payload includes the recipient's freshly recomputed unread count for the lobby badge and for the in-game header.

Lifecycle hooks (Stage B)

The lobby module is the producer of system mail. Stage B will add a DiplomailPublisher collaborator on lobby.Service and call it on paused / cancelled transitions and on BlockMembership / AdminBanMember. The publisher constructs a kind='admin', sender_kind='system' message with a templated body; the recipient receives the durable copy in their inbox even after the membership is revoked.

If a future stage adds inactivity-based player removal at the lobby sweeper, that path must call the same publisher so the kicked player has the explanation in their inbox.

Wiring

cmd/backend/main.go constructs *diplomail.Service with three collaborators:

  • *Store over the shared Postgres pool;
  • MembershipLookup adapter that walks the lobby cache for the active (game_id, user_id) row and stitches in the immutable accounts.user_name;
  • NotificationPublisher adapter that translates each DiplomailNotification into a notification.Intent and routes it through *notification.Service.Submit.