11 Commits

Author SHA1 Message Date
Ilia Denisov 1556d36511 Phase 28: mark stage done after CI gate green
Tests · Integration / integration (pull_request) Successful in 1m43s
Tests · Go / test (pull_request) Successful in 2m4s
Tests · UI / test (pull_request) Successful in 2m20s
Gitea runs at commit 6d0272b:
- go-unit #134 → success
- ui-test #136 → success
- integration #135 → success

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:56:29 +02:00
Ilia Denisov 6d0272b078 Phase 28 (Step 11): Vitest coverage for MailStore threading
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m43s
Tests · Go / test (pull_request) Successful in 2m1s
Tests · UI / test (pull_request) Successful in 2m24s
`tests/mail-store.test.ts` exercises the `entries` derived rune
with handcrafted inbox + sent fixtures:

- personal messages exchanged with one race collapse into a
  per-race thread with messages sorted oldest → newest;
- system mail (`sender_kind=system`) and admin notifications
  (`sender_kind=admin`) surface as stand-alone items even when a
  race-name snapshot is present;
- the caller's own paid-tier broadcasts (`broadcast_scope=
  game_broadcast`) render as stand-alone outgoing items;
- `unreadCount` counts inbox rows with `readAt === null`.

The store fields are mutated directly to avoid wiring a fake
`GalaxyClient`; the underlying `$derived` rune fires whenever
those fields change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:50:01 +02:00
Ilia Denisov c48bc83890 Phase 28 (Step 10): docs — diplomail UI topic + FUNCTIONAL mirror
Tests · UI / test (pull_request) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m4s
- `ui/docs/diplomail-ui.md`: new topic doc covering the wire
  surface, recipient-by-race-name decision, threading model,
  translation toggle, push events, badge, layout, and
  accessibility.
- `docs/FUNCTIONAL.md` §11.4 grows a paragraph that records the
  UI's per-race threading rule, the absent read-receipt UX, and
  the recipient-by-race-name compose path. Mirrored verbatim into
  `docs/FUNCTIONAL_ru.md`.
- `ui/PLAN.md` Phase 28 marked done with a "Decisions during
  stage" block matching the implementation plan, and the artifact
  list updated to the actual file set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:48:16 +02:00
Ilia Denisov db81bd8e08 Phase 28 (Steps 7+8): header unread badge + push/init wiring
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 3m25s
Step 7 — header view-menu badge.

`view-menu.svelte` reads `mailStore.unreadCount` and renders an
inline pill next to the "diplomatic mail" entry whenever the
counter is non-zero. The badge styling matches the per-row dot in
`thread-list.svelte` so the two surfaces feel consistent.

Step 8 — push event handler + MailStore init in the in-game layout.

`routes/games/[id]/+layout.svelte`:

- registers a `diplomail.message.received` handler alongside the
  existing `game.turn.ready` / `game.paused` ones, parses the
  signed payload, calls `mailStore.applyPushEvent` to refresh the
  inbox for the matching game, and raises a toast with a "view"
  deep-link that navigates to `/games/:id/mail`;
- adds `mailStore.init({ client, cache, gameId })` to the boot
  `Promise.all` so the inbox + sent lists are warm by the time the
  view mounts, and the badge counter is populated before any user
  interaction;
- disposes the new subscription in the `onDestroy` block so a game
  switch does not leak handlers across navigations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:46:00 +02:00
Ilia Denisov f7300f25a3 Phase 28 (Steps 6+9): mail active view + i18n keys
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m36s
Tests · Go / test (pull_request) Successful in 3m19s
Tests · UI / test (pull_request) Waiting to run
Step 6 — mail active view + subcomponents.

- `lib/active-view/mail.svelte` replaces the Phase 10 stub with the
  list / detail layout: two-pane on desktop, one-pane stack on
  mobile (CSS media query, no separate route).
- `lib/active-view/mail/thread-list.svelte` renders per-race
  threads collapsed to their last message plus stand-alone
  system / admin / outgoing-broadcast items, with unread badges.
- `lib/active-view/mail/thread-pane.svelte` is the chat-style
  transcript for one race; bodies render through `textContent`,
  per-message Show original / translation toggles flip the
  rendering when a translated body is present, and a persistent
  reply box at the bottom calls `mailStore.composePersonal`.
- `lib/active-view/mail/system-item-pane.svelte` renders one
  stand-alone item read-only with the same translation toggle.
- `lib/active-view/mail/compose.svelte` is the compose dialog:
  recipient race picker fed from `report.races[]`, kind toggle
  (personal / broadcast / admin), admin sub-toggle for target
  user / all and recipient-scope picker. Server-side enforces
  paid-tier and owner gating; the UI surfaces 403 inline.
- `lib/active-view/mail/system-titles.ts` keeps the keyword →
  i18n-title mapping for lifecycle-hook system mail so both the
  list and the detail pane pick the same canonical title.

Step 9 — i18n strings (en + ru).

`game.mail.*`, `game.view.mail.badge`, `game.events.mail_new.*`,
`game.mail.system.*` keys added in lockstep across both locales
covering compose labels / validation copy / per-system titles /
translation toggle / reply / delete affordances.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:43:09 +02:00
Ilia Denisov fdd5fd193d Phase 28 (Step 5): MailStore reactive state
Tests · UI / test (pull_request) Waiting to run
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m38s
Tests · Go / test (pull_request) Successful in 3m19s
Adds `src/lib/mail-store.svelte.ts` — the reactive store that
coordinates the in-game mail view. Responsibilities:

- holds the inbox and sent listings for the current game and fires
  the initial parallel fetch (`fetchInbox` + `fetchSent`) on
  `setGame`;
- exposes a `entries` derived rune that builds the unified list
  pane: per-race threads merged from incoming + outgoing personal
  messages, plus stand-alone items for system / admin / own
  paid-tier broadcasts. Thread messages are sorted oldest → newest
  for chat-style rendering; the list itself sorts newest-first by
  the most-recent entry timestamp;
- derives `unreadCount` from `readAt === null` rows for the header
  view-menu badge;
- imperative `markRead` / `softDelete` actions with optimistic
  state flips and roll-back on RPC failure;
- compose actions for personal / paid-tier broadcast / owner-admin
  sends;
- `applyPushEvent(gameId)` hook called by the layout when a
  `diplomail.message.received` push frame arrives; refetches the
  inbox without trusting the preview payload;
- persists the most recent message id under
  `cache.diplomail/${gameId}/last-seen` so a returning session can
  pre-paint the badge without a network round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:37:32 +02:00
Ilia Denisov 7378d4c8ed Phase 28 (Step 4): UI api/diplomail.ts wrappers
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m20s
Tests · Integration / integration (pull_request) Successful in 1m43s
Adds typed wrappers around `GalaxyClient.executeCommand` for the
eight Phase 28 mail RPCs. Each wrapper builds the matching
FlatBuffers request, decodes the response, and surfaces backend
errors through a dedicated `MailError` (mirroring `LobbyError`).
The compose helpers accept the recipient race name directly so the
UI can feed it straight from `report.races[].name` without a
membership lookup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:35:21 +02:00
Ilia Denisov 4cb03736de Phase 28 (Step 3): gateway translators for user.games.mail.*
Tests · Integration / integration (pull_request) Successful in 1m55s
Tests · Go / test (push) Successful in 2m10s
Tests · Go / test (pull_request) Successful in 2m11s
Tests · UI / test (pull_request) Waiting to run
Adds the gateway-side translation layer that maps the eight new
ConnectRPC mail commands onto backend's
`/api/v1/user/games/{game_id}/mail/*` REST endpoints.

- `gateway/internal/backendclient/mail_commands.go` defines
  `ExecuteMailCommand` and one helper per command (inbox, sent,
  message.get, send, broadcast, admin, read, delete). Each helper
  decodes the FlatBuffers request envelope, issues the REST call
  via the existing `*RESTClient.do`, decodes the JSON body, and
  re-encodes a typed FlatBuffers response. Recipient identifiers
  travel through unchanged so the new `recipient_race_name`
  shortcut introduced in Step 1 reaches backend untouched.
- `routes.go` exposes a `MailRoutes` constructor and a matching
  `mailCommandClient` implementing `downstream.Client`.
- `cmd/gateway/main.go` registers the new routes alongside the
  existing user / lobby / game-engine routes.
- `mail_commands_test.go` covers the inbox, send-by-race-name, and
  read-state paths end-to-end against an `httptest.Server`,
  asserting request shapes (path, body, X-User-ID) and the
  decoded FlatBuffers response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:32:50 +02:00
Ilia Denisov 57d2286f5e Phase 28 (Step 3a): /sent returns full message detail per recipient
Tests · Go / test (push) Successful in 2m5s
Tests · Go / test (pull_request) Successful in 2m10s
Tests · Integration / integration (pull_request) Successful in 1m54s
Tests · UI / test (pull_request) Successful in 2m53s
Phase 28's in-game mail UI threads sent messages by the recipient
race name, so the bulk `/sent` endpoint now returns the same
`UserMailMessageDetail` shape as `/inbox` — single sends contribute
one row per message, broadcasts contribute one row per addressee
and the UI collapses them by `message_id` into a stand-alone item.

- `Store.ListSent` / `Service.ListSent` switched from `[]Message`
  to `[]InboxEntry`. SQL grows an INNER JOIN with
  `diplomail_recipients`.
- Handler emits `userMailMessageDetailWire` items; the deprecated
  `userMailSentSummaryWire` is removed.
- `openapi.yaml`: `UserMailSentList.items` now reference
  `UserMailMessageDetail`; the standalone `UserMailSentSummary`
  schema is dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:27:39 +02:00
Ilia Denisov fed282f2d2 Phase 28 (Step 2): FBS schemas + message-type constants for mail
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m4s
Tests · Go / test (push) Successful in 2m5s
Tests · Integration / integration (pull_request) Successful in 1m54s
Tests · UI / test (pull_request) Successful in 2m50s
Adds the wire schema for the eight `user.games.mail.*` ConnectRPC
commands together with the shared payload types (`MailMessage`,
`MailRecipientState`, `MailBroadcastReceipt`). Send-request tables
carry the optional `recipient_race_name` introduced in Step 1.

Drops:

- `pkg/schema/fbs/diplomail.fbs` — schema sources;
- `pkg/schema/fbs/diplomail/*.go` — generated Go bindings (flatc
  `--go --go-module-name galaxy/schema/fbs`);
- `pkg/model/diplomail/diplomail.go` — message-type catalog used by
  the gateway router;
- `ui/frontend/src/proto/galaxy/fbs/diplomail/*.ts` — generated TS
  bindings consumed by the upcoming UI client wrapper;
- `ui/Makefile` `FBS_INPUTS` extended to pick the new schema up on
  the next `make -C ui fbs-ts` run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:21:23 +02:00
Ilia Denisov 7b43ce5844 Phase 28 (Step 1): backend support for race-name mail send
Tests · Go / test (push) Successful in 1m56s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · Go / test (pull_request) Successful in 2m2s
Phase 28's in-game mail UI groups personal threads by the other
party's race. To support that without an extra membership-listing
RPC, the diplomail subsystem now:

- accepts `recipient_race_name` on `POST /messages` and
  `POST /admin` (target=user) as an alternative to
  `recipient_user_id`; the service resolves it via the existing
  `Memberships.ListMembers(gameID, "active")` and rejects with
  `forbidden` when the matching member is no longer active;
- snapshots `diplomail_messages.sender_race_name` at send time for
  every player sender (admin / system rows stay NULL). The UI keys
  per-race threading on this column.

Schema, openapi, README, and a focused e2e test for the new path
(happy path + dual / missing / unknown / kicked errors) land in
this commit; the gateway + UI legs follow in subsequent commits on
this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:07:48 +02:00
75 changed files with 7886 additions and 189 deletions
+22 -1
View File
@@ -26,7 +26,10 @@ 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.
rendering survives renames and purges. The `sender_race_name`
column snapshots the sender's race in the game at send time when
the sender is a player with an active membership; the in-game UI
keys per-race thread grouping on this column.
- `diplomail_recipients` — one row per (message, recipient). Holds
per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at`
state. Snapshot fields (`recipient_user_name`,
@@ -72,6 +75,24 @@ mail to every active member; `Service.changeMembershipStatus` /
`detector.LanguageDetector` (default: `whatlanggo`, body-only,
≥ 25 runes; shorter bodies stay `und`).
## Recipient selection
`POST /messages` and `POST /admin` (when `target="user"`) accept the
recipient identifier in one of two shapes:
- `recipient_user_id` (uuid) — explicit user lookup; the recipient
may be any active member of the game.
- `recipient_race_name` (string) — resolves to the active member
with this race name in the game. Race names are unique by lobby
invariant; lobby-removed and blocked members cannot be reached
through the race-name shortcut (they no longer appear in the
active scope). Exactly one of the two fields must be supplied;
supplying both, or neither, returns `invalid_request`.
The race-name path lets the in-game UI compose mail directly off
the engine's `report.races[]` view without an extra membership
round-trip.
## Translation
Stage D adds a lazy translation cache. When a recipient reads a
+27 -8
View File
@@ -29,7 +29,11 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp
return Message{}, Recipient{}, err
}
recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, in.RecipientUserID)
recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName)
if err != nil {
return Message{}, Recipient{}, err
}
recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, recipientID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not a member of the game", ErrForbidden)
@@ -37,7 +41,7 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp
return Message{}, Recipient{}, fmt.Errorf("diplomail: load admin recipient: %w", err)
}
msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername,
msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername,
recipient.GameID, recipient.GameName, subject, body, in.SenderIP, BroadcastScopeSingle)
if err != nil {
return Message{}, Recipient{}, err
@@ -84,7 +88,7 @@ func (s *Service) SendAdminBroadcast(ctx context.Context, in SendAdminBroadcastI
}
gameName := members[0].GameName
msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername,
msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername,
in.GameID, gameName, subject, body, in.SenderIP, BroadcastScopeGameBroadcast)
if err != nil {
return Message{}, nil, err
@@ -147,6 +151,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
}
username := sender.UserName
senderRace := sender.RaceName
msgInsert := MessageInsert{
MessageID: uuid.New(),
GameID: in.GameID,
@@ -155,6 +160,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
SenderKind: SenderKindPlayer,
SenderUserID: &callerID,
SenderUsername: &username,
SenderRaceName: &senderRace,
SenderIP: in.SenderIP,
Subject: subject,
Body: body,
@@ -217,7 +223,7 @@ func (s *Service) SendAdminMultiGameBroadcast(ctx context.Context, in SendMultiG
zap.String("scope", scope))
continue
}
msgInsert, err := s.buildAdminMessageInsert(CallerKindAdmin, nil, in.CallerUsername,
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindAdmin, nil, in.CallerUsername,
game.GameID, game.GameName, subject, body, in.SenderIP, BroadcastScopeMultiGameBroadcast)
if err != nil {
return nil, 0, err
@@ -356,7 +362,7 @@ func (s *Service) publishGameLifecycle(ctx context.Context, ev LifecycleEvent) e
gameName := members[0].GameName
subject, body := renderGameLifecycle(ev.Kind, gameName, ev.Actor, ev.Reason)
msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "",
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "",
ev.GameID, gameName, subject, body, "", BroadcastScopeGameBroadcast)
if err != nil {
return err
@@ -385,7 +391,7 @@ func (s *Service) publishMembershipLifecycle(ctx context.Context, ev LifecycleEv
}
subject, body := renderMembershipLifecycle(ev.Kind, target.GameName, ev.Actor, ev.Reason)
msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "",
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "",
ev.GameID, target.GameName, subject, body, "", BroadcastScopeSingle)
if err != nil {
return err
@@ -417,10 +423,12 @@ func (s *Service) prepareContent(subject, body string) (string, string, error) {
// for every admin-kind send. The CHECK constraint maps sender
// shapes:
//
// sender_kind='player' → CallerKind owner; sender_user_id set
// sender_kind='player' → CallerKind owner; sender_user_id set,
// sender_race_name resolved from
// Memberships.GetActiveMembership
// sender_kind='admin' → CallerKind admin; sender_user_id nil
// sender_kind='system' → CallerKind system; sender_username nil
func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.UUID, callerUsername string,
func (s *Service) buildAdminMessageInsert(ctx context.Context, callerKind string, callerUserID *uuid.UUID, callerUsername string,
gameID uuid.UUID, gameName, subject, body, senderIP, scope string) (MessageInsert, error) {
out := MessageInsert{
MessageID: uuid.New(),
@@ -443,6 +451,17 @@ func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.
out.SenderKind = SenderKindPlayer
out.SenderUserID = &uid
out.SenderUsername = &uname
// Owner race snapshot is best-effort: a private-game owner who
// has an active membership in their own game contributes a
// race name; an owner who is not a current member (or whose
// membership is removed/blocked) leaves the field nil. The
// CHECK constraint accepts both shapes for sender_kind='player'.
if ownerMember, err := s.deps.Memberships.GetActiveMembership(ctx, gameID, uid); err == nil {
race := ownerMember.RaceName
out.SenderRaceName = &race
} else if !errors.Is(err, ErrNotFound) {
return MessageInsert{}, fmt.Errorf("diplomail: load owner membership: %w", err)
}
case CallerKindAdmin:
uname := callerUsername
out.SenderKind = SenderKindAdmin
@@ -369,6 +369,114 @@ func TestDiplomailPersonalFlow(t *testing.T) {
}
}
// TestDiplomailPersonalByRaceName exercises the Phase 28 contract: the
// UI passes a recipient race name (read out of the game report); the
// service resolves it to the active member with that race name and
// snapshots the sender's race onto the message row. Error cases cover
// the validation rules baked into the wire schema.
func TestDiplomailPersonalByRaceName(t *testing.T) {
db := startPostgres(t)
ctx := context.Background()
gameID := uuid.New()
sender := uuid.New()
recipient := uuid.New()
kicked := uuid.New()
seedAccount(t, db, sender)
seedAccount(t, db, recipient)
seedAccount(t, db, kicked)
seedGame(t, db, gameID, "Race-Name Resolution Game")
lookup := &staticMembershipLookup{
rows: map[[2]uuid.UUID]diplomail.ActiveMembership{
{gameID, sender}: {
UserID: sender, GameID: gameID, GameName: "Race-Name Resolution Game",
UserName: "sender", RaceName: "Senders",
},
{gameID, recipient}: {
UserID: recipient, GameID: gameID, GameName: "Race-Name Resolution Game",
UserName: "recipient", RaceName: "Receivers",
},
},
inactive: map[[2]uuid.UUID]diplomail.MemberSnapshot{
{gameID, kicked}: {
UserID: kicked, GameID: gameID, GameName: "Race-Name Resolution Game",
UserName: "kicked", RaceName: "Departed", Status: "removed",
},
},
}
svc := diplomail.NewService(diplomail.Deps{
Store: diplomail.NewStore(db),
Memberships: lookup,
Notification: &recordingPublisher{},
Config: config.DiplomailConfig{
MaxBodyBytes: 4096,
MaxSubjectBytes: 256,
},
})
// Happy path: race name resolves and sender_race_name is snapshotted.
msg, rcpt, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: sender,
RecipientRaceName: "Receivers",
Subject: "Trade",
Body: "Care to talk?",
SenderIP: "203.0.113.4",
})
if err != nil {
t.Fatalf("send by race name: %v", err)
}
if rcpt.UserID != recipient {
t.Fatalf("recipient = %s, want %s", rcpt.UserID, recipient)
}
if msg.SenderRaceName == nil || *msg.SenderRaceName != "Senders" {
t.Fatalf("sender_race_name = %v, want \"Senders\"", msg.SenderRaceName)
}
// Both identifiers supplied → invalid_request.
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: sender,
RecipientUserID: recipient,
RecipientRaceName: "Receivers",
Body: "x",
}); !errors.Is(err, diplomail.ErrInvalidInput) {
t.Fatalf("dual identifier: %v, want ErrInvalidInput", err)
}
// Neither identifier supplied → invalid_request.
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: sender,
Body: "x",
}); !errors.Is(err, diplomail.ErrInvalidInput) {
t.Fatalf("no identifier: %v, want ErrInvalidInput", err)
}
// Race name with no matching active member → invalid_request.
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: sender,
RecipientRaceName: "Strangers",
Body: "x",
}); !errors.Is(err, diplomail.ErrInvalidInput) {
t.Fatalf("unknown race: %v, want ErrInvalidInput", err)
}
// Race name of a lobby-removed member → invalid_request (the
// active-only scope filters them out; the lookup never returns
// them).
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: sender,
RecipientRaceName: "Departed",
Body: "x",
}); !errors.Is(err, diplomail.ErrInvalidInput) {
t.Fatalf("kicked race: %v, want ErrInvalidInput", err)
}
}
func TestDiplomailRejectsNonActiveSender(t *testing.T) {
db := startPostgres(t)
ctx := context.Background()
+62 -10
View File
@@ -32,15 +32,20 @@ const previewMaxRunes = 120
// ErrForbidden; the inserted Message is never persisted in those
// cases.
func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Message, Recipient, error) {
if in.SenderUserID == in.RecipientUserID {
return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput)
}
subject := strings.TrimRight(in.Subject, " \t")
body := strings.TrimRight(in.Body, " \t\n")
if err := s.validateContent(subject, body); err != nil {
return Message{}, Recipient{}, err
}
recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName)
if err != nil {
return Message{}, Recipient{}, err
}
if in.SenderUserID == recipientID {
return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput)
}
sender, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.SenderUserID)
if err != nil {
if errors.Is(err, ErrNotFound) {
@@ -48,7 +53,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
}
return Message{}, Recipient{}, fmt.Errorf("diplomail: load sender membership: %w", err)
}
recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.RecipientUserID)
recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, recipientID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not an active member of the game", ErrForbidden)
@@ -57,14 +62,17 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
}
username := sender.UserName
senderRace := sender.RaceName
senderUserID := in.SenderUserID
msgInsert := MessageInsert{
MessageID: uuid.New(),
GameID: in.GameID,
GameName: sender.GameName,
Kind: KindPersonal,
SenderKind: SenderKindPlayer,
SenderUserID: &in.SenderUserID,
SenderUserID: &senderUserID,
SenderUsername: &username,
SenderRaceName: &senderRace,
SenderIP: in.SenderIP,
Subject: subject,
Body: body,
@@ -75,7 +83,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
rcptInsert := buildRecipientInsert(
msgInsert.MessageID,
MemberSnapshot{
UserID: in.RecipientUserID,
UserID: recipientID,
GameID: in.GameID,
GameName: recipient.GameName,
UserName: recipient.UserName,
@@ -101,6 +109,47 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
return msg, recipients[0], nil
}
// resolveActiveRecipient turns a (user_id, race_name) pair into the
// canonical user id of an active member of gameID. Exactly one of the
// two inputs must be set; both-set or both-empty returns
// ErrInvalidInput. Race-name resolution is restricted to the active
// scope so lobby-removed and blocked members cannot be reached
// through the race-name shortcut. ErrInvalidInput is also returned
// when the race name matches zero members; ErrForbidden when the
// race name matches more than one active row (defence in depth — race
// names are unique within a game by lobby invariant).
func (s *Service) resolveActiveRecipient(ctx context.Context, gameID uuid.UUID, byUserID uuid.UUID, byRaceName string) (uuid.UUID, error) {
byRaceName = strings.TrimSpace(byRaceName)
hasUser := byUserID != uuid.Nil
hasRace := byRaceName != ""
switch {
case hasUser && hasRace:
return uuid.Nil, fmt.Errorf("%w: only one of recipient_user_id, recipient_race_name may be supplied", ErrInvalidInput)
case !hasUser && !hasRace:
return uuid.Nil, fmt.Errorf("%w: recipient_user_id or recipient_race_name must be supplied", ErrInvalidInput)
case hasUser:
return byUserID, nil
}
members, err := s.deps.Memberships.ListMembers(ctx, gameID, RecipientScopeActive)
if err != nil {
return uuid.Nil, fmt.Errorf("diplomail: list active members for race lookup: %w", err)
}
var found []MemberSnapshot
for _, m := range members {
if m.RaceName == byRaceName {
found = append(found, m)
}
}
switch len(found) {
case 0:
return uuid.Nil, fmt.Errorf("%w: no active member with race %q in this game", ErrInvalidInput, byRaceName)
case 1:
return found[0].UserID, nil
default:
return uuid.Nil, fmt.Errorf("%w: race %q matches multiple active members", ErrForbidden, byRaceName)
}
}
// GetMessage returns the InboxEntry for messageID addressed to
// userID. ErrNotFound is returned when the caller is not a recipient
// of the message — handlers translate that to 404 so the existence
@@ -267,10 +316,13 @@ func (s *Service) allowedKinds(ctx context.Context, gameID, userID uuid.UUID) (m
return map[string]bool{KindAdmin: true}, nil
}
// ListSent returns personal messages authored by senderUserID in
// gameID, newest first. Admin/system rows have no `sender_user_id`
// and are therefore excluded; the user surface does not need them.
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
// ListSent returns the sender-side view of personal messages
// authored by senderUserID in gameID, newest first. Each entry pairs
// the message with one of its recipient rows; single sends contribute
// one entry per message, broadcasts contribute one entry per
// addressee. Admin and system rows have no `sender_user_id` and are
// therefore excluded; the user surface does not need them.
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
return s.deps.Store.ListSent(ctx, gameID, senderUserID)
}
+33 -14
View File
@@ -31,7 +31,7 @@ func messageColumns() postgres.ColumnList {
m := table.DiplomailMessages
return postgres.ColumnList{
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind,
m.SenderUserID, m.SenderUsername, m.SenderIP,
m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP,
m.Subject, m.Body, m.BodyLang, m.BroadcastScope, m.CreatedAt,
}
}
@@ -59,6 +59,7 @@ type MessageInsert struct {
SenderKind string
SenderUserID *uuid.UUID
SenderUsername *string
SenderRaceName *string
SenderIP string
Subject string
Body string
@@ -101,7 +102,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
m := table.DiplomailMessages
msgStmt := m.INSERT(
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind,
m.SenderUserID, m.SenderUsername, m.SenderIP,
m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP,
m.Subject, m.Body, m.BodyLang, m.BroadcastScope,
).VALUES(
msg.MessageID,
@@ -111,6 +112,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
msg.SenderKind,
uuidPtrArg(msg.SenderUserID),
stringPtrArg(msg.SenderUsername),
stringPtrArg(msg.SenderRaceName),
msg.SenderIP,
msg.Subject,
msg.Body,
@@ -241,25 +243,38 @@ func (s *Store) ListInbox(ctx context.Context, gameID, userID uuid.UUID) ([]Inbo
return out, nil
}
// ListSent returns messages authored by senderUserID in gameID,
// newest first. Personal messages only — admin/system rows have
// `sender_user_id IS NULL` and are filtered out by the WHERE clause.
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
// ListSent returns the sender-side view of personal messages
// authored by senderUserID in gameID, newest first. Each
// `InboxEntry` carries the message together with one of its
// recipient rows — single sends produce one entry per message;
// game broadcasts produce one entry per addressee (the in-game
// mail UI collapses broadcast entries into a single stand-alone
// item by `message_id`). Admin / system rows have
// `sender_user_id IS NULL` and are excluded by the WHERE clause.
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
m := table.DiplomailMessages
stmt := postgres.SELECT(messageColumns()).
FROM(m).
r := table.DiplomailRecipients
cols := append(messageColumns(), recipientColumns()...)
stmt := postgres.SELECT(cols).
FROM(m.INNER_JOIN(r, r.MessageID.EQ(m.MessageID))).
WHERE(
m.GameID.EQ(postgres.UUID(gameID)).
AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))),
).
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC())
var rows []model.DiplomailMessages
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC(), r.RecipientID.ASC())
var dest []struct {
model.DiplomailMessages
Recipient model.DiplomailRecipients `alias:"diplomail_recipients"`
}
if err := stmt.QueryContext(ctx, s.db, &dest); err != nil {
return nil, fmt.Errorf("diplomail store: list sent %s/%s: %w", gameID, senderUserID, err)
}
out := make([]Message, 0, len(rows))
for _, row := range rows {
out = append(out, messageFromModel(row))
out := make([]InboxEntry, 0, len(dest))
for _, row := range dest {
out = append(out, InboxEntry{
Message: messageFromModel(row.DiplomailMessages),
Recipient: recipientFromModel(row.Recipient),
})
}
return out, nil
}
@@ -737,6 +752,10 @@ func messageFromModel(row model.DiplomailMessages) Message {
name := *row.SenderUsername
out.SenderUsername = &name
}
if row.SenderRaceName != nil {
name := *row.SenderRaceName
out.SenderRaceName = &name
}
return out
}
+31 -19
View File
@@ -23,6 +23,11 @@ type Message struct {
SenderKind string
SenderUserID *uuid.UUID
SenderUsername *string
// SenderRaceName carries the snapshot of the sender's race in the
// game at send time. Non-nil for sender_kind='player' rows, nil
// for admin and system. The in-game mail UI groups personal
// threads by this name (Phase 28).
SenderRaceName *string
SenderIP string
Subject string
Body string
@@ -92,16 +97,19 @@ type Translation struct {
}
// SendPersonalInput is the request payload for SendPersonal: the
// caller sending a single-recipient personal message. Validation
// (active membership, body length, etc.) is performed inside the
// service.
// caller sending a single-recipient personal message. Exactly one of
// RecipientUserID and RecipientRaceName must be non-zero; the
// service resolves a non-empty RecipientRaceName to the active
// member with that race in the game. Other validation (active
// membership, body length, etc.) is performed inside the service.
type SendPersonalInput struct {
GameID uuid.UUID
SenderUserID uuid.UUID
RecipientUserID uuid.UUID
Subject string
Body string
SenderIP string
GameID uuid.UUID
SenderUserID uuid.UUID
RecipientUserID uuid.UUID
RecipientRaceName string
Subject string
Body string
SenderIP string
}
// CallerKind enumerates the privileged sender roles for admin-kind
@@ -116,17 +124,21 @@ const (
// SendAdminPersonalInput is the request payload for an owner /
// admin / system sending an admin-kind message to a single
// recipient. Authorization (owner-vs-admin distinction) is enforced
// by the HTTP layer; the service trusts the caller designation.
// recipient. Exactly one of RecipientUserID and RecipientRaceName
// must be non-zero; the service resolves a non-empty
// RecipientRaceName to the active member with that race in the
// game. Authorization (owner-vs-admin distinction) is enforced by
// the HTTP layer; the service trusts the caller designation.
type SendAdminPersonalInput struct {
GameID uuid.UUID
CallerKind string
CallerUserID *uuid.UUID
CallerUsername string
RecipientUserID uuid.UUID
Subject string
Body string
SenderIP string
GameID uuid.UUID
CallerKind string
CallerUserID *uuid.UUID
CallerUsername string
RecipientUserID uuid.UUID
RecipientRaceName string
Subject string
Body string
SenderIP string
}
// SendAdminBroadcastInput is the request payload for an owner /
@@ -20,6 +20,7 @@ type DiplomailMessages struct {
SenderKind string
SenderUserID *uuid.UUID
SenderUsername *string
SenderRaceName *string
SenderIP string
Subject string
Body string
@@ -24,6 +24,7 @@ type diplomailMessagesTable struct {
SenderKind postgres.ColumnString
SenderUserID postgres.ColumnString
SenderUsername postgres.ColumnString
SenderRaceName postgres.ColumnString
SenderIP postgres.ColumnString
Subject postgres.ColumnString
Body postgres.ColumnString
@@ -78,14 +79,15 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
SenderKindColumn = postgres.StringColumn("sender_kind")
SenderUserIDColumn = postgres.StringColumn("sender_user_id")
SenderUsernameColumn = postgres.StringColumn("sender_username")
SenderRaceNameColumn = postgres.StringColumn("sender_race_name")
SenderIPColumn = postgres.StringColumn("sender_ip")
SubjectColumn = postgres.StringColumn("subject")
BodyColumn = postgres.StringColumn("body")
BodyLangColumn = postgres.StringColumn("body_lang")
BroadcastScopeColumn = postgres.StringColumn("broadcast_scope")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{SenderIPColumn, SubjectColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
)
@@ -100,6 +102,7 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
SenderKind: SenderKindColumn,
SenderUserID: SenderUserIDColumn,
SenderUsername: SenderUsernameColumn,
SenderRaceName: SenderRaceNameColumn,
SenderIP: SenderIPColumn,
Subject: SubjectColumn,
Body: BodyColumn,
@@ -683,6 +683,11 @@ CREATE TABLE diplomail_messages (
sender_kind text NOT NULL,
sender_user_id uuid,
sender_username text,
-- sender_race_name is the immutable snapshot of the sender's race
-- in this game, captured at insert time when sender_kind='player'.
-- Admin and system messages carry NULL. The Phase 28 mail UI keys
-- per-race threading on this column.
sender_race_name text,
sender_ip text NOT NULL DEFAULT '',
subject text NOT NULL DEFAULT '',
body text NOT NULL,
@@ -698,6 +703,13 @@ CREATE TABLE diplomail_messages (
(sender_kind = 'admin' AND sender_user_id IS NULL AND sender_username IS NOT NULL) OR
(sender_kind = 'system' AND sender_user_id IS NULL AND sender_username IS NULL)
),
-- sender_race_name is only meaningful for player senders. Admin
-- and system rows never carry a race; player rows carry one when
-- the sender has an active membership at send time (a non-playing
-- private-game owner may legitimately have none).
CONSTRAINT diplomail_messages_sender_race_chk CHECK (
sender_kind = 'player' OR sender_race_name IS NULL
),
CONSTRAINT diplomail_messages_kind_sender_chk CHECK (
(kind = 'personal' AND sender_kind = 'player') OR
(kind = 'admin' AND sender_kind IN ('player', 'admin', 'system'))
@@ -60,19 +60,24 @@ func (h *AdminDiplomailHandlers) Send() gin.HandlerFunc {
ctx := c.Request.Context()
switch req.Target {
case "", "user":
recipientID, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
var recipientID uuid.UUID
if req.RecipientUserID != "" {
parsed, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
}
recipientID = parsed
}
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
GameID: gameID,
CallerKind: diplomail.CallerKindAdmin,
CallerUsername: username,
RecipientUserID: recipientID,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
GameID: gameID,
CallerKind: diplomail.CallerKindAdmin,
CallerUsername: username,
RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
})
if sendErr != nil {
respondDiplomailError(c, h.logger, "admin mail send personal", ctx, sendErr)
+62 -66
View File
@@ -87,19 +87,24 @@ func (h *UserMailHandlers) SendPersonal() gin.HandlerFunc {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON")
return
}
recipientID, err := uuid.Parse(req.RecipientUserID)
if err != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
var recipientID uuid.UUID
if req.RecipientUserID != "" {
parsed, perr := uuid.Parse(req.RecipientUserID)
if perr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
}
recipientID = parsed
}
ctx := c.Request.Context()
msg, rcpt, err := h.svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID,
SenderUserID: userID,
RecipientUserID: recipientID,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
GameID: gameID,
SenderUserID: userID,
RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
})
if err != nil {
respondDiplomailError(c, h.logger, "user mail send personal", ctx, err)
@@ -189,9 +194,9 @@ func (h *UserMailHandlers) Sent() gin.HandlerFunc {
respondDiplomailError(c, h.logger, "user mail sent", ctx, err)
return
}
out := userMailSentListWire{Items: make([]userMailSentSummaryWire, 0, len(items))}
for _, m := range items {
out.Items = append(out.Items, mailMessageSummaryToWire(m))
out := userMailSentListWire{Items: make([]userMailMessageDetailWire, 0, len(items))}
for _, entry := range items {
out.Items = append(out.Items, mailMessageDetailToWire(entry, false))
}
c.JSON(http.StatusOK, out)
}
@@ -341,21 +346,26 @@ func (h *UserMailHandlers) SendAdmin() gin.HandlerFunc {
switch req.Target {
case "", "user":
recipientID, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
var recipientID uuid.UUID
if req.RecipientUserID != "" {
parsed, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return
}
recipientID = parsed
}
callerUserID := userID
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
GameID: gameID,
CallerKind: diplomail.CallerKindOwner,
CallerUserID: &callerUserID,
CallerUsername: account.UserName,
RecipientUserID: recipientID,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
GameID: gameID,
CallerKind: diplomail.CallerKindOwner,
CallerUserID: &callerUserID,
CallerUsername: account.UserName,
RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject,
Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c),
})
if sendErr != nil {
respondDiplomailError(c, h.logger, "user mail send admin personal", ctx, sendErr)
@@ -449,10 +459,13 @@ func parseMessageIDParam(c *gin.Context) (uuid.UUID, bool) {
}
// userMailSendRequestWire mirrors the request body for SendPersonal.
// Exactly one of `recipient_user_id` and `recipient_race_name` must
// be supplied; the service rejects ambiguous and empty inputs.
type userMailSendRequestWire struct {
RecipientUserID string `json:"recipient_user_id"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
}
// userMailSendBroadcastRequestWire mirrors the request body for the
@@ -464,15 +477,16 @@ type userMailSendBroadcastRequestWire struct {
}
// userMailSendAdminRequestWire mirrors the request body for the
// owner-only admin send. `target="user"` requires
// `recipient_user_id`; `target="all"` accepts the optional
// `recipients` scope (default `active`).
// owner-only admin send. `target="user"` requires exactly one of
// `recipient_user_id` and `recipient_race_name`; `target="all"`
// accepts the optional `recipients` scope (default `active`).
type userMailSendAdminRequestWire struct {
Target string `json:"target"`
RecipientUserID string `json:"recipient_user_id,omitempty"`
Recipients string `json:"recipients,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
Target string `json:"target"`
RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Recipients string `json:"recipients,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
}
// userMailBroadcastReceiptWire is the response shape returned after a
@@ -524,6 +538,7 @@ type userMailMessageDetailWire struct {
SenderKind string `json:"sender_kind"`
SenderUserID *string `json:"sender_user_id,omitempty"`
SenderUsername *string `json:"sender_username,omitempty"`
SenderRaceName *string `json:"sender_race_name,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
BodyLang string `json:"body_lang"`
@@ -540,27 +555,18 @@ type userMailMessageDetailWire struct {
Translator *string `json:"translator,omitempty"`
}
// userMailSentSummaryWire mirrors the response shape for the
// sender-side listing. Recipient state is intentionally omitted (one
// author may have N recipients per broadcast in later stages).
type userMailSentSummaryWire struct {
MessageID string `json:"message_id"`
GameID string `json:"game_id"`
GameName string `json:"game_name,omitempty"`
Kind string `json:"kind"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
BodyLang string `json:"body_lang"`
BroadcastScope string `json:"broadcast_scope"`
CreatedAt string `json:"created_at"`
}
type userMailInboxListWire struct {
Items []userMailMessageDetailWire `json:"items"`
}
// userMailSentListWire mirrors the response shape for the
// sender-side listing. Phase 28's in-game UI threads sent messages
// by the recipient's race name, so the wire carries the full
// message detail (including the recipient snapshot) — single sends
// contribute one row per message, broadcasts contribute one row per
// addressee and the UI collapses them by `message_id`.
type userMailSentListWire struct {
Items []userMailSentSummaryWire `json:"items"`
Items []userMailMessageDetailWire `json:"items"`
}
type userMailUnreadCountWire struct {
@@ -597,6 +603,10 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
s := *entry.SenderUsername
out.SenderUsername = &s
}
if entry.SenderRaceName != nil {
s := *entry.SenderRaceName
out.SenderRaceName = &s
}
if entry.Recipient.RecipientRaceName != nil {
s := *entry.Recipient.RecipientRaceName
out.RecipientRaceName = &s
@@ -624,20 +634,6 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
return out
}
func mailMessageSummaryToWire(m diplomail.Message) userMailSentSummaryWire {
return userMailSentSummaryWire{
MessageID: m.MessageID.String(),
GameID: m.GameID.String(),
GameName: m.GameName,
Kind: m.Kind,
Subject: m.Subject,
Body: m.Body,
BodyLang: m.BodyLang,
BroadcastScope: m.BroadcastScope,
CreatedAt: m.CreatedAt.UTC().Format(timestampLayout),
}
}
// mailRecipientStateToWire renders the recipient row after a
// mark-read or soft-delete call. The caller only needs the per-user
// state, not the full message body again.
+43 -41
View File
@@ -4068,11 +4068,22 @@ components:
UserMailSendRequest:
type: object
additionalProperties: false
required: [recipient_user_id, body]
required: [body]
properties:
recipient_user_id:
type: string
format: uuid
description: |
Either `recipient_user_id` or `recipient_race_name` must
be supplied; supplying both is rejected as
`invalid_request`.
recipient_race_name:
type: string
description: |
Resolves to the active member with this race name in the
game. Mutually exclusive with `recipient_user_id`. The
server returns `forbidden` when the matching member is no
longer active (lobby-removed / blocked).
subject:
type: string
description: |
@@ -4093,10 +4104,18 @@ components:
type: string
format: uuid
description: |
Required when `target="user"`. Identifies the recipient
of the personal admin message; the recipient may be in
any membership status (admin notifications can reach
kicked players).
One of `recipient_user_id` and `recipient_race_name` is
required when `target="user"`. Identifies the recipient
of the personal admin message by uuid; the recipient may
be in any membership status (admin notifications can
reach kicked players when addressed by user_id).
recipient_race_name:
type: string
description: |
Optional alternative to `recipient_user_id` when
`target="user"`. Resolves to the active member with this
race name in the game; lobby-removed and blocked members
cannot be reached through the race-name shortcut.
recipients:
type: string
enum: [active, active_and_removed, all_members]
@@ -4323,6 +4342,17 @@ components:
sender_username:
type: string
nullable: true
sender_race_name:
type: string
nullable: true
description: |
Snapshot of the sender's race name in this game at send
time. Populated when `sender_kind="player"` and the
sender had an active membership at send time; nil for
admin and system messages, and for player messages sent
by a private-game owner who was not an active member at
send time. The in-game UI keys per-race threading on this
field.
subject:
type: string
body:
@@ -4370,41 +4400,6 @@ components:
translator:
type: string
description: Identifier of the translation engine that produced the cached row.
UserMailSentSummary:
type: object
additionalProperties: false
required:
- message_id
- game_id
- kind
- body
- body_lang
- broadcast_scope
- created_at
properties:
message_id:
type: string
format: uuid
game_id:
type: string
format: uuid
game_name:
type: string
kind:
type: string
enum: [personal, admin]
subject:
type: string
body:
type: string
body_lang:
type: string
broadcast_scope:
type: string
enum: [single, game_broadcast, multi_game_broadcast]
created_at:
type: string
format: date-time
UserMailInboxList:
type: object
additionalProperties: false
@@ -4415,6 +4410,13 @@ components:
items:
$ref: "#/components/schemas/UserMailMessageDetail"
UserMailSentList:
description: |
Sender-side listing of personal messages authored by the
caller. Each item carries the same shape as inbox entries
(including the recipient snapshot); single sends contribute
one row per message, broadcasts contribute one row per
addressee so the in-game UI can collapse them by
`message_id` into a single stand-alone item.
type: object
additionalProperties: false
required: [items]
@@ -4422,7 +4424,7 @@ components:
items:
type: array
items:
$ref: "#/components/schemas/UserMailSentSummary"
$ref: "#/components/schemas/UserMailMessageDetail"
UserMailUnreadCount:
type: object
additionalProperties: false
+14
View File
@@ -1270,6 +1270,20 @@ The message detail response includes both the original body and,
when available, the cached translation; the client UI defaults to
the translated text and offers a "show original" toggle.
The in-game UI groups personal mail into per-race threads — every
personal message exchanged between the local player and another
race lands in one thread keyed on the other party's race. System
mail, admin notifications, and the player's own paid-tier
broadcasts render as stand-alone entries in the same list pane and
are never threaded. `read_at` and `deleted_at` drive the local
unread counter and the soft-delete affordance but are not surfaced
to the user — diplomatic mail does not promise read receipts. The
compose form picks the recipient by race name (resolved
server-side from `Memberships.ListMembers(game_id, "active")`); no
client-side memberships listing is fetched. See
[`ui/docs/diplomail-ui.md`](../ui/docs/diplomail-ui.md) for the
detailed UI breakdown.
### 11.5 Lifecycle hooks
Three lobby transitions land as system mail in the affected
+14
View File
@@ -1309,6 +1309,20 @@ bulk-purge всей почты соответствующей партии.
кэш) перевод; UI по умолчанию показывает перевод и предлагает
переключение «показать оригинал».
Внутриигровой UI группирует личную почту по веткам по расам —
каждая личная переписка между локальным игроком и другой расой
оказывается в одной ветке, ключевая по расе собеседника.
Системные сообщения, административные уведомления и собственные
рассылки игрока (платный тариф) показываются отдельными
автономными записями в том же списке и никогда не группируются.
`read_at` и `deleted_at` поддерживают локальный счётчик
непрочитанного и кнопку удаления, но не показываются игроку —
дипломатическая почта не обещает уведомления о прочтении. Форма
compose выбирает получателя по имени расы (сервер резолвит через
`Memberships.ListMembers(game_id, "active")`); клиент не тянет
отдельный список членов. Подробнее — в
[`ui/docs/diplomail-ui.md`](../ui/docs/diplomail-ui.md).
### 11.5 Хуки жизненного цикла
Три транзитных перехода в лобби порождают system mail в inbox
+5 -1
View File
@@ -186,7 +186,8 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
userRoutes := backendclient.UserRoutes(backend.REST())
lobbyRoutes := backendclient.LobbyRoutes(backend.REST())
gameRoutes := backendclient.GameRoutes(backend.REST())
allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes))
mailRoutes := backendclient.MailRoutes(backend.REST())
allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes)+len(mailRoutes))
for k, v := range userRoutes {
allRoutes[k] = v
}
@@ -196,6 +197,9 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
for k, v := range gameRoutes {
allRoutes[k] = v
}
for k, v := range mailRoutes {
allRoutes[k] = v
}
cleanup := func() error {
return closeRedisClient()
@@ -0,0 +1,567 @@
package backendclient
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"galaxy/gateway/internal/downstream"
diplomailmodel "galaxy/model/diplomail"
commonfbs "galaxy/schema/fbs/common"
fbs "galaxy/schema/fbs/diplomail"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
)
// ExecuteMailCommand routes one authenticated `user.games.mail.*`
// command into the matching `/api/v1/user/games/{game_id}/mail/...`
// backend REST endpoint. Each command decodes a FlatBuffers request
// payload, issues the REST call, decodes the JSON response, and
// re-encodes the result as a typed FlatBuffers envelope.
func (c *RESTClient) ExecuteMailCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
if c == nil || c.httpClient == nil {
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil client")
}
if ctx == nil {
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil context")
}
if err := ctx.Err(); err != nil {
return downstream.UnaryResult{}, err
}
if strings.TrimSpace(command.UserID) == "" {
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: user_id must not be empty")
}
switch command.MessageType {
case diplomailmodel.MessageTypeUserGamesMailInbox:
return c.executeMailInbox(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailSent:
return c.executeMailSent(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailMessageGet:
return c.executeMailMessageGet(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailSend:
return c.executeMailSend(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailBroadcast:
return c.executeMailBroadcast(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailAdmin:
return c.executeMailAdmin(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailRead:
return c.executeMailRead(ctx, command.UserID, command.PayloadBytes)
case diplomailmodel.MessageTypeUserGamesMailDelete:
return c.executeMailDelete(ctx, command.UserID, command.PayloadBytes)
default:
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute mail command: unsupported message type %q", command.MessageType)
}
}
// mailMessageJSON mirrors the backend's `UserMailMessageDetail` wire
// shape from `backend/openapi.yaml`. Pointer fields are nullable in
// the OpenAPI spec; the encoder treats empty strings as "absent".
type mailMessageJSON struct {
MessageID string `json:"message_id"`
GameID string `json:"game_id"`
GameName string `json:"game_name,omitempty"`
Kind string `json:"kind"`
SenderKind string `json:"sender_kind"`
SenderUserID *string `json:"sender_user_id,omitempty"`
SenderUsername *string `json:"sender_username,omitempty"`
SenderRaceName *string `json:"sender_race_name,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
BodyLang string `json:"body_lang"`
BroadcastScope string `json:"broadcast_scope"`
CreatedAt string `json:"created_at"`
RecipientUserID string `json:"recipient_user_id"`
RecipientUserName string `json:"recipient_user_name,omitempty"`
RecipientRaceName *string `json:"recipient_race_name,omitempty"`
ReadAt *string `json:"read_at,omitempty"`
DeletedAt *string `json:"deleted_at,omitempty"`
TranslatedSubject *string `json:"translated_subject,omitempty"`
TranslatedBody *string `json:"translated_body,omitempty"`
TranslationLang *string `json:"translation_lang,omitempty"`
Translator *string `json:"translator,omitempty"`
}
// mailRecipientStateJSON mirrors `UserMailRecipientState`.
type mailRecipientStateJSON struct {
MessageID string `json:"message_id"`
ReadAt *string `json:"read_at,omitempty"`
DeletedAt *string `json:"deleted_at,omitempty"`
}
// mailBroadcastReceiptJSON mirrors `UserMailBroadcastReceipt`.
type mailBroadcastReceiptJSON struct {
MessageID string `json:"message_id"`
GameID string `json:"game_id"`
GameName string `json:"game_name,omitempty"`
Kind string `json:"kind"`
SenderKind string `json:"sender_kind"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
BodyLang string `json:"body_lang"`
BroadcastScope string `json:"broadcast_scope"`
CreatedAt string `json:"created_at"`
RecipientCount int `json:"recipient_count"`
}
type mailInboxJSON struct {
Items []mailMessageJSON `json:"items"`
}
func (c *RESTClient) executeMailInbox(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: payload is empty")
}
flat := fbs.GetRootAsInboxRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
if gameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: game_id is missing")
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/inbox"
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.inbox: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
var resp mailInboxJSON
if err := json.Unmarshal(respBody, &resp); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail inbox response: %w", err)
}
out := encodeMailMessageList(resp.Items, fbs.InboxResponseStart, fbs.InboxResponseAddItems, fbs.InboxResponseEnd, fbs.FinishInboxResponseBuffer)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
}
func (c *RESTClient) executeMailSent(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: payload is empty")
}
flat := fbs.GetRootAsSentRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
if gameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: game_id is missing")
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/sent"
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.sent: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
var resp mailInboxJSON
if err := json.Unmarshal(respBody, &resp); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail sent response: %w", err)
}
out := encodeMailMessageList(resp.Items, fbs.SentResponseStart, fbs.SentResponseAddItems, fbs.SentResponseEnd, fbs.FinishSentResponseBuffer)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
}
func (c *RESTClient) executeMailMessageGet(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: payload is empty")
}
flat := fbs.GetRootAsMessageGetRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
messageID := readUUID(flat.MessageId(nil))
if gameID == uuid.Nil || messageID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: game_id and message_id are required")
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.message.get: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
var msg mailMessageJSON
if err := json.Unmarshal(respBody, &msg); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail message response: %w", err)
}
builder := flatbuffers.NewBuilder(512)
msgOff := encodeMailMessage(builder, &msg)
fbs.MessageGetResponseStart(builder)
fbs.MessageGetResponseAddMessage(builder, msgOff)
root := fbs.MessageGetResponseEnd(builder)
fbs.FinishMessageGetResponseBuffer(builder, root)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}
func (c *RESTClient) executeMailSend(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: payload is empty")
}
flat := fbs.GetRootAsSendRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
if gameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: game_id is missing")
}
body := struct {
RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
}{
RecipientUserID: string(flat.RecipientUserId()),
RecipientRaceName: string(flat.RecipientRaceName()),
Subject: string(flat.Subject()),
Body: string(flat.Body()),
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages"
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.send: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
var msg mailMessageJSON
if err := json.Unmarshal(respBody, &msg); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail send response: %w", err)
}
builder := flatbuffers.NewBuilder(512)
msgOff := encodeMailMessage(builder, &msg)
fbs.SendResponseStart(builder)
fbs.SendResponseAddMessage(builder, msgOff)
root := fbs.SendResponseEnd(builder)
fbs.FinishSendResponseBuffer(builder, root)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}
func (c *RESTClient) executeMailBroadcast(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: payload is empty")
}
flat := fbs.GetRootAsBroadcastRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
if gameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: game_id is missing")
}
body := struct {
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
}{
Subject: string(flat.Subject()),
Body: string(flat.Body()),
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/broadcast"
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.broadcast: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
var receipt mailBroadcastReceiptJSON
if err := json.Unmarshal(respBody, &receipt); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail broadcast response: %w", err)
}
builder := flatbuffers.NewBuilder(256)
recOff := encodeMailBroadcastReceipt(builder, &receipt)
fbs.BroadcastResponseStart(builder)
fbs.BroadcastResponseAddReceipt(builder, recOff)
root := fbs.BroadcastResponseEnd(builder)
fbs.FinishBroadcastResponseBuffer(builder, root)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}
func (c *RESTClient) executeMailAdmin(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: payload is empty")
}
flat := fbs.GetRootAsAdminRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
if gameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: game_id is missing")
}
target := string(flat.Target())
body := struct {
Target string `json:"target"`
RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Recipients string `json:"recipients,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body"`
}{
Target: target,
RecipientUserID: string(flat.RecipientUserId()),
RecipientRaceName: string(flat.RecipientRaceName()),
Recipients: string(flat.Recipients()),
Subject: string(flat.Subject()),
Body: string(flat.Body()),
}
url := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/admin"
respBody, status, err := c.do(ctx, http.MethodPost, url, userID, body)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.admin: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
builder := flatbuffers.NewBuilder(512)
if target == "all" {
var receipt mailBroadcastReceiptJSON
if err := json.Unmarshal(respBody, &receipt); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail admin broadcast response: %w", err)
}
recOff := encodeMailBroadcastReceipt(builder, &receipt)
fbs.AdminResponseStart(builder)
fbs.AdminResponseAddReceipt(builder, recOff)
root := fbs.AdminResponseEnd(builder)
fbs.FinishAdminResponseBuffer(builder, root)
} else {
var msg mailMessageJSON
if err := json.Unmarshal(respBody, &msg); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail admin send response: %w", err)
}
msgOff := encodeMailMessage(builder, &msg)
fbs.AdminResponseStart(builder)
fbs.AdminResponseAddMessage(builder, msgOff)
root := fbs.AdminResponseEnd(builder)
fbs.FinishAdminResponseBuffer(builder, root)
}
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}
func (c *RESTClient) executeMailRead(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: payload is empty")
}
flat := fbs.GetRootAsReadRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
messageID := readUUID(flat.MessageId(nil))
if gameID == uuid.Nil || messageID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: game_id and message_id are required")
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String()) + "/read"
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, struct{}{})
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.read: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
return encodeRecipientStateResponse(respBody, fbs.ReadResponseStart, fbs.ReadResponseAddState, fbs.ReadResponseEnd, fbs.FinishReadResponseBuffer)
}
func (c *RESTClient) executeMailDelete(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
if len(payload) == 0 {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: payload is empty")
}
flat := fbs.GetRootAsDeleteRequest(payload, 0)
gameID := readUUID(flat.GameId(nil))
messageID := readUUID(flat.MessageId(nil))
if gameID == uuid.Nil || messageID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: game_id and message_id are required")
}
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
respBody, status, err := c.do(ctx, http.MethodDelete, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.delete: %w", err)
}
if status < 200 || status >= 300 {
return projectMailErrorResponse(status, respBody)
}
return encodeRecipientStateResponse(respBody, fbs.DeleteResponseStart, fbs.DeleteResponseAddState, fbs.DeleteResponseEnd, fbs.FinishDeleteResponseBuffer)
}
// encodeRecipientStateResponse decodes the JSON recipient-state body
// and emits the corresponding FlatBuffers Read/Delete envelope. The
// caller supplies the trio of envelope start / add-state / end / finish
// functions so this helper covers both endpoints with the same shape.
func encodeRecipientStateResponse(respBody []byte,
startFn func(*flatbuffers.Builder),
addStateFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
) (downstream.UnaryResult, error) {
var state mailRecipientStateJSON
if err := json.Unmarshal(respBody, &state); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode mail recipient state: %w", err)
}
builder := flatbuffers.NewBuilder(128)
stateOff := encodeMailRecipientState(builder, &state)
startFn(builder)
addStateFn(builder, stateOff)
root := endFn(builder)
finishFn(builder, root)
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
}
// encodeMailMessageList is a shared helper that encodes a slice of
// mailMessageJSON items into either an InboxResponse or a
// SentResponse FlatBuffers envelope. The two envelopes have the same
// shape (just a `items` vector of MailMessage) so the trio of
// constructor functions parameterises the helper.
func encodeMailMessageList(items []mailMessageJSON,
startFn func(*flatbuffers.Builder),
addItemsFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
) []byte {
builder := flatbuffers.NewBuilder(1024)
offsets := make([]flatbuffers.UOffsetT, 0, len(items))
for i := range items {
offsets = append(offsets, encodeMailMessage(builder, &items[i]))
}
// FlatBuffers vectors are built in reverse: prepend each offset.
builder.StartVector(4, len(offsets), 4)
for i := len(offsets) - 1; i >= 0; i-- {
builder.PrependUOffsetT(offsets[i])
}
itemsVec := builder.EndVector(len(offsets))
startFn(builder)
addItemsFn(builder, itemsVec)
root := endFn(builder)
finishFn(builder, root)
return builder.FinishedBytes()
}
// encodeMailMessage builds a MailMessage table inside builder. Returns
// the offset of the finished table. Strings are interned through the
// builder; missing JSON fields (nil pointers, empty strings) yield
// empty FB strings which the readers treat as absent.
func encodeMailMessage(builder *flatbuffers.Builder, m *mailMessageJSON) flatbuffers.UOffsetT {
messageIDOff := builder.CreateString(m.MessageID)
gameIDOff := builder.CreateString(m.GameID)
gameNameOff := builder.CreateString(m.GameName)
kindOff := builder.CreateString(m.Kind)
senderKindOff := builder.CreateString(m.SenderKind)
senderUserIDOff := builder.CreateString(stringPtrValue(m.SenderUserID))
senderUsernameOff := builder.CreateString(stringPtrValue(m.SenderUsername))
senderRaceNameOff := builder.CreateString(stringPtrValue(m.SenderRaceName))
subjectOff := builder.CreateString(m.Subject)
bodyOff := builder.CreateString(m.Body)
bodyLangOff := builder.CreateString(m.BodyLang)
broadcastScopeOff := builder.CreateString(m.BroadcastScope)
recipientUserIDOff := builder.CreateString(m.RecipientUserID)
recipientUserNameOff := builder.CreateString(m.RecipientUserName)
recipientRaceNameOff := builder.CreateString(stringPtrValue(m.RecipientRaceName))
translatedSubjectOff := builder.CreateString(stringPtrValue(m.TranslatedSubject))
translatedBodyOff := builder.CreateString(stringPtrValue(m.TranslatedBody))
translationLangOff := builder.CreateString(stringPtrValue(m.TranslationLang))
translatorOff := builder.CreateString(stringPtrValue(m.Translator))
fbs.MailMessageStart(builder)
fbs.MailMessageAddMessageId(builder, messageIDOff)
fbs.MailMessageAddGameId(builder, gameIDOff)
fbs.MailMessageAddGameName(builder, gameNameOff)
fbs.MailMessageAddKind(builder, kindOff)
fbs.MailMessageAddSenderKind(builder, senderKindOff)
fbs.MailMessageAddSenderUserId(builder, senderUserIDOff)
fbs.MailMessageAddSenderUsername(builder, senderUsernameOff)
fbs.MailMessageAddSenderRaceName(builder, senderRaceNameOff)
fbs.MailMessageAddSubject(builder, subjectOff)
fbs.MailMessageAddBody(builder, bodyOff)
fbs.MailMessageAddBodyLang(builder, bodyLangOff)
fbs.MailMessageAddBroadcastScope(builder, broadcastScopeOff)
fbs.MailMessageAddCreatedAtMs(builder, parseRFC3339Millis(m.CreatedAt))
fbs.MailMessageAddRecipientUserId(builder, recipientUserIDOff)
fbs.MailMessageAddRecipientUserName(builder, recipientUserNameOff)
fbs.MailMessageAddRecipientRaceName(builder, recipientRaceNameOff)
fbs.MailMessageAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(m.ReadAt)))
fbs.MailMessageAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(m.DeletedAt)))
fbs.MailMessageAddTranslatedSubject(builder, translatedSubjectOff)
fbs.MailMessageAddTranslatedBody(builder, translatedBodyOff)
fbs.MailMessageAddTranslationLang(builder, translationLangOff)
fbs.MailMessageAddTranslator(builder, translatorOff)
return fbs.MailMessageEnd(builder)
}
// encodeMailRecipientState builds a MailRecipientState table.
func encodeMailRecipientState(builder *flatbuffers.Builder, s *mailRecipientStateJSON) flatbuffers.UOffsetT {
messageIDOff := builder.CreateString(s.MessageID)
fbs.MailRecipientStateStart(builder)
fbs.MailRecipientStateAddMessageId(builder, messageIDOff)
fbs.MailRecipientStateAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(s.ReadAt)))
fbs.MailRecipientStateAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(s.DeletedAt)))
return fbs.MailRecipientStateEnd(builder)
}
// encodeMailBroadcastReceipt builds a MailBroadcastReceipt table.
func encodeMailBroadcastReceipt(builder *flatbuffers.Builder, r *mailBroadcastReceiptJSON) flatbuffers.UOffsetT {
messageIDOff := builder.CreateString(r.MessageID)
gameIDOff := builder.CreateString(r.GameID)
gameNameOff := builder.CreateString(r.GameName)
kindOff := builder.CreateString(r.Kind)
senderKindOff := builder.CreateString(r.SenderKind)
subjectOff := builder.CreateString(r.Subject)
bodyOff := builder.CreateString(r.Body)
bodyLangOff := builder.CreateString(r.BodyLang)
broadcastScopeOff := builder.CreateString(r.BroadcastScope)
fbs.MailBroadcastReceiptStart(builder)
fbs.MailBroadcastReceiptAddMessageId(builder, messageIDOff)
fbs.MailBroadcastReceiptAddGameId(builder, gameIDOff)
fbs.MailBroadcastReceiptAddGameName(builder, gameNameOff)
fbs.MailBroadcastReceiptAddKind(builder, kindOff)
fbs.MailBroadcastReceiptAddSenderKind(builder, senderKindOff)
fbs.MailBroadcastReceiptAddSubject(builder, subjectOff)
fbs.MailBroadcastReceiptAddBody(builder, bodyOff)
fbs.MailBroadcastReceiptAddBodyLang(builder, bodyLangOff)
fbs.MailBroadcastReceiptAddBroadcastScope(builder, broadcastScopeOff)
fbs.MailBroadcastReceiptAddCreatedAtMs(builder, parseRFC3339Millis(r.CreatedAt))
fbs.MailBroadcastReceiptAddRecipientCount(builder, int32(r.RecipientCount))
return fbs.MailBroadcastReceiptEnd(builder)
}
// projectMailErrorResponse maps a non-2xx response into a UnaryResult
// carrying the backend error envelope, reusing the shared user-mail
// error-projection. 503 is bubbled as ErrDownstreamUnavailable.
func projectMailErrorResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
if statusCode == http.StatusServiceUnavailable {
return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
}
if statusCode >= 400 && statusCode <= 599 {
return projectUserBackendError(statusCode, payload)
}
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
}
// readUUID converts the common.UUID struct (or its absence) into a
// google/uuid.UUID. Returns uuid.Nil when the input is nil.
func readUUID(u *commonfbs.UUID) uuid.UUID {
if u == nil {
return uuid.Nil
}
var out uuid.UUID
hi := u.Hi()
lo := u.Lo()
for i := 0; i < 8; i++ {
out[i] = byte(hi >> (56 - 8*i))
out[i+8] = byte(lo >> (56 - 8*i))
}
return out
}
// stringPtrValue returns "" for nil and the dereferenced value
// otherwise. Used to flatten nullable JSON strings into the
// always-present FlatBuffers string slot.
func stringPtrValue(p *string) string {
if p == nil {
return ""
}
return *p
}
// parseRFC3339Millis parses an RFC 3339 timestamp string (the format
// the backend mail handler emits) into Unix milliseconds. Returns 0
// when the input is empty or unparseable, matching the "absent"
// convention for the *_at_ms wire fields.
func parseRFC3339Millis(s string) int64 {
if s == "" {
return 0
}
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return 0
}
return t.UnixMilli()
}
@@ -0,0 +1,209 @@
package backendclient_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"galaxy/gateway/internal/backendclient"
diplomailmodel "galaxy/model/diplomail"
commonfbs "galaxy/schema/fbs/common"
fbs "galaxy/schema/fbs/diplomail"
flatbuffers "github.com/google/flatbuffers/go"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExecuteMailInboxDecodesItems(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("11111111-2222-3333-4444-555555555555")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodGet, r.Method)
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/inbox", r.URL.Path)
require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID))
writeJSON(t, w, http.StatusOK, map[string]any{
"items": []map[string]any{
{
"message_id": "00000000-0000-0000-0000-000000000001",
"game_id": gameID.String(),
"kind": "personal",
"sender_kind": "player",
"sender_user_id": "00000000-0000-0000-0000-000000000010",
"sender_username": "alice",
"sender_race_name": "AliceRace",
"subject": "hi",
"body": "hello there",
"body_lang": "en",
"broadcast_scope": "single",
"created_at": "2026-05-15T12:00:00Z",
"recipient_user_id": "00000000-0000-0000-0000-000000000020",
"recipient_user_name": "bob",
"recipient_race_name": "BobRace",
},
},
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload := buildInboxRequest(gameID)
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailInbox, payload)
result, err := client.ExecuteMailCommand(context.Background(), cmd)
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
resp := fbs.GetRootAsInboxResponse(result.PayloadBytes, 0)
require.Equal(t, 1, resp.ItemsLength())
var item fbs.MailMessage
require.True(t, resp.Items(&item, 0))
assert.Equal(t, "00000000-0000-0000-0000-000000000001", string(item.MessageId()))
assert.Equal(t, "AliceRace", string(item.SenderRaceName()))
assert.Equal(t, "BobRace", string(item.RecipientRaceName()))
}
func TestExecuteMailSendForwardsRaceName(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("22222222-3333-4444-5555-666666666666")
var captured struct {
Body string
RecipientUserID string
RecipientRaceName string
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages", r.URL.Path)
var req map[string]any
raw, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(raw, &req))
if v, ok := req["body"].(string); ok {
captured.Body = v
}
if v, ok := req["recipient_user_id"].(string); ok {
captured.RecipientUserID = v
}
if v, ok := req["recipient_race_name"].(string); ok {
captured.RecipientRaceName = v
}
writeJSON(t, w, http.StatusCreated, map[string]any{
"message_id": "00000000-0000-0000-0000-000000000099",
"game_id": gameID.String(),
"kind": "personal",
"sender_kind": "player",
"sender_user_id": "00000000-0000-0000-0000-000000000010",
"sender_race_name": "Senders",
"body": captured.Body,
"body_lang": "en",
"broadcast_scope": "single",
"created_at": "2026-05-15T12:00:00Z",
"recipient_user_id": "00000000-0000-0000-0000-000000000020",
"recipient_race_name": "Receivers",
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload := buildSendRequestByRaceName(gameID, "Receivers", "let us talk")
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailSend, payload)
result, err := client.ExecuteMailCommand(context.Background(), cmd)
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
resp := fbs.GetRootAsSendResponse(result.PayloadBytes, 0)
require.NotNil(t, resp.Message(nil))
msg := resp.Message(nil)
assert.Equal(t, "let us talk", string(msg.Body()))
assert.Equal(t, "Senders", string(msg.SenderRaceName()))
assert.Equal(t, "Receivers", string(msg.RecipientRaceName()))
assert.Empty(t, captured.RecipientUserID)
assert.Equal(t, "Receivers", captured.RecipientRaceName)
assert.Equal(t, "let us talk", captured.Body)
}
func TestExecuteMailReadReturnsState(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("33333333-4444-5555-6666-777777777777")
messageID := uuid.MustParse("00000000-0000-0000-0000-0000000000aa")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages/"+messageID.String()+"/read", r.URL.Path)
writeJSON(t, w, http.StatusOK, map[string]any{
"message_id": messageID.String(),
"read_at": "2026-05-15T12:34:56Z",
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload := buildReadRequest(gameID, messageID)
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailRead, payload)
result, err := client.ExecuteMailCommand(context.Background(), cmd)
require.NoError(t, err)
resp := fbs.GetRootAsReadResponse(result.PayloadBytes, 0)
state := resp.State(nil)
require.NotNil(t, state)
assert.Equal(t, messageID.String(), string(state.MessageId()))
assert.NotZero(t, state.ReadAtMs())
}
// buildInboxRequest emits a FlatBuffers InboxRequest envelope with
// the supplied game_id.
func buildInboxRequest(gameID uuid.UUID) []byte {
builder := flatbuffers.NewBuilder(64)
hi, lo := uuidToHiLo(gameID)
fbs.InboxRequestStart(builder)
fbs.InboxRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
root := fbs.InboxRequestEnd(builder)
fbs.FinishInboxRequestBuffer(builder, root)
return builder.FinishedBytes()
}
// buildSendRequestByRaceName emits a FlatBuffers SendRequest that
// addresses the recipient by race name rather than user_id.
func buildSendRequestByRaceName(gameID uuid.UUID, raceName, body string) []byte {
builder := flatbuffers.NewBuilder(128)
raceOff := builder.CreateString(raceName)
bodyOff := builder.CreateString(body)
hi, lo := uuidToHiLo(gameID)
fbs.SendRequestStart(builder)
fbs.SendRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
fbs.SendRequestAddRecipientRaceName(builder, raceOff)
fbs.SendRequestAddBody(builder, bodyOff)
root := fbs.SendRequestEnd(builder)
fbs.FinishSendRequestBuffer(builder, root)
return builder.FinishedBytes()
}
// buildReadRequest emits a FlatBuffers ReadRequest envelope.
func buildReadRequest(gameID, messageID uuid.UUID) []byte {
builder := flatbuffers.NewBuilder(64)
gameHi, gameLo := uuidToHiLo(gameID)
msgHi, msgLo := uuidToHiLo(messageID)
fbs.ReadRequestStart(builder)
fbs.ReadRequestAddGameId(builder, commonfbs.CreateUUID(builder, gameHi, gameLo))
fbs.ReadRequestAddMessageId(builder, commonfbs.CreateUUID(builder, msgHi, msgLo))
root := fbs.ReadRequestEnd(builder)
fbs.FinishReadRequestBuffer(builder, root)
return builder.FinishedBytes()
}
// uuidToHiLo splits a 16-byte UUID into the two big-endian uint64
// halves the common.UUID struct uses.
func uuidToHiLo(u uuid.UUID) (uint64, uint64) {
var hi, lo uint64
for i := 0; i < 8; i++ {
hi = (hi << 8) | uint64(u[i])
lo = (lo << 8) | uint64(u[i+8])
}
return hi, lo
}
+31
View File
@@ -4,6 +4,7 @@ import (
"context"
"galaxy/gateway/internal/downstream"
diplomailmodel "galaxy/model/diplomail"
lobbymodel "galaxy/model/lobby"
ordermodel "galaxy/model/order"
reportmodel "galaxy/model/report"
@@ -67,6 +68,27 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client {
}
}
// MailRoutes returns the authenticated `user.games.mail.*` downstream
// routes served by backend's diplomail subsystem. When client is nil
// every route resolves to a dependency-unavailable client so the
// static router still recognises the message types.
func MailRoutes(client *RESTClient) map[string]downstream.Client {
target := downstream.Client(unavailableClient{})
if client != nil {
target = mailCommandClient{rest: client}
}
return map[string]downstream.Client{
diplomailmodel.MessageTypeUserGamesMailInbox: target,
diplomailmodel.MessageTypeUserGamesMailSent: target,
diplomailmodel.MessageTypeUserGamesMailMessageGet: target,
diplomailmodel.MessageTypeUserGamesMailSend: target,
diplomailmodel.MessageTypeUserGamesMailBroadcast: target,
diplomailmodel.MessageTypeUserGamesMailAdmin: target,
diplomailmodel.MessageTypeUserGamesMailRead: target,
diplomailmodel.MessageTypeUserGamesMailDelete: target,
}
}
type unavailableClient struct{}
func (unavailableClient) ExecuteCommand(context.Context, downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
@@ -97,9 +119,18 @@ func (c gameCommandClient) ExecuteCommand(ctx context.Context, command downstrea
return c.rest.ExecuteGameCommand(ctx, command)
}
type mailCommandClient struct {
rest *RESTClient
}
func (c mailCommandClient) ExecuteCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
return c.rest.ExecuteMailCommand(ctx, command)
}
var (
_ downstream.Client = unavailableClient{}
_ downstream.Client = userCommandClient{}
_ downstream.Client = lobbyCommandClient{}
_ downstream.Client = gameCommandClient{}
_ downstream.Client = mailCommandClient{}
)
+56
View File
@@ -0,0 +1,56 @@
// Package diplomail defines the public typed command identifiers
// exposed at the authenticated Gateway -> Diplomatic Mail boundary.
//
// The gateway routes each `user.games.mail.*` ExecuteCommand into the
// matching `/api/v1/user/games/{game_id}/mail/*` REST endpoint on the
// backend; the wire envelopes and payload tables live in
// `pkg/schema/fbs/diplomail.fbs`.
package diplomail
const (
// MessageTypeUserGamesMailInbox is the authenticated gateway
// message type used to read the caller's diplomatic-mail inbox
// for one game. Backend filters out rows whose `available_at` is
// still nil (translation in flight).
MessageTypeUserGamesMailInbox = "user.games.mail.inbox"
// MessageTypeUserGamesMailSent is the authenticated gateway
// message type used to read the caller's outgoing personal
// messages for one game. Admin and system rows are not included.
MessageTypeUserGamesMailSent = "user.games.mail.sent"
// MessageTypeUserGamesMailMessageGet is the authenticated
// gateway message type used to read a single message detail
// addressed to the caller. The response carries the translation
// rendering when one is cached for the caller's preferred
// language.
MessageTypeUserGamesMailMessageGet = "user.games.mail.message.get"
// MessageTypeUserGamesMailSend is the authenticated gateway
// message type used to send a single-recipient personal message.
// Exactly one of `recipient_user_id` and `recipient_race_name`
// must be supplied; the backend resolves the race-name shortcut
// through `Memberships.ListMembers(gameID, "active")`.
MessageTypeUserGamesMailSend = "user.games.mail.send"
// MessageTypeUserGamesMailBroadcast is the authenticated gateway
// message type used by paid-tier callers to broadcast a personal
// message to every other active member of the game.
MessageTypeUserGamesMailBroadcast = "user.games.mail.broadcast"
// MessageTypeUserGamesMailAdmin is the authenticated gateway
// message type used by the game owner to compose an admin-kind
// notification. The wire shape is target-discriminated: `user`
// addresses a single recipient (by id or race name); `all`
// broadcasts to every member matching the requested scope.
MessageTypeUserGamesMailAdmin = "user.games.mail.admin"
// MessageTypeUserGamesMailRead is the authenticated gateway
// message type used to mark a single message as read. Idempotent.
MessageTypeUserGamesMailRead = "user.games.mail.read"
// MessageTypeUserGamesMailDelete is the authenticated gateway
// message type used to soft-delete a single message. The
// recipient row must already be marked read.
MessageTypeUserGamesMailDelete = "user.games.mail.delete"
)
+196
View File
@@ -0,0 +1,196 @@
// diplomail contains FlatBuffers payloads used by the authenticated
// gateway boundary for the in-game diplomatic-mail subsystem. The
// wire shapes here mirror the trusted internal
// `/api/v1/user/games/{game_id}/mail/*` REST surface; gateway derives
// the calling `user_id` from the verified session and forwards it as
// `X-User-Id` to backend.
include "common.fbs";
namespace diplomail;
// MailMessage stores one inbox / sent-list / message-detail row. The
// fields mirror `UserMailMessageDetail` in `backend/openapi.yaml`
// with the following encoding rules:
//
// - `*_user_id` fields are RFC 4122 string UUIDs ("" means absent
// for nullable fields such as `sender_user_id`).
// - `*_at_ms` fields carry Unix milliseconds; `0` means the
// timestamp is absent (e.g. an unread message has
// `read_at_ms == 0`).
// - `translated_*`, `translation_lang`, and `translator` are set
// when the backend served a cached rendering into the caller's
// preferred language; empty otherwise.
// - `sender_race_name` is the snapshot of the sender's race name
// in this game at send time. Present for `sender_kind="player"`
// messages when the sender had an active membership; absent for
// admin and system messages. The in-game UI keys per-race
// threading on this field.
table MailMessage {
message_id:string;
game_id:string;
game_name:string;
kind:string;
sender_kind:string;
sender_user_id:string;
sender_username:string;
sender_race_name:string;
subject:string;
body:string;
body_lang:string;
broadcast_scope:string;
created_at_ms:int64;
recipient_user_id:string;
recipient_user_name:string;
recipient_race_name:string;
read_at_ms:int64;
deleted_at_ms:int64;
translated_subject:string;
translated_body:string;
translation_lang:string;
translator:string;
}
// MailRecipientState mirrors the `UserMailRecipientState` payload
// returned from mark-read and soft-delete endpoints. Same timestamp
// conventions as `MailMessage`.
table MailRecipientState {
message_id:string;
read_at_ms:int64;
deleted_at_ms:int64;
}
// MailBroadcastReceipt mirrors `UserMailBroadcastReceipt`. Returned
// from broadcast sends (paid-tier and admin); `recipient_count` is
// the number of recipient rows the server materialised.
table MailBroadcastReceipt {
message_id:string;
game_id:string;
game_name:string;
kind:string;
sender_kind:string;
subject:string;
body:string;
body_lang:string;
broadcast_scope:string;
created_at_ms:int64;
recipient_count:int32;
}
// InboxRequest stores the read-side request for the caller's inbox
// in `game_id`. Backend filters to messages with `available_at` set
// (translation completed when the recipient's preferred language
// differs from the body language).
table InboxRequest {
game_id:common.UUID (required);
}
// InboxResponse stores the resulting inbox list, newest first.
// `items` is empty when the caller has no available messages in
// this game.
table InboxResponse {
items:[MailMessage];
}
// SentRequest stores the read-side request for the caller's sent
// personal messages in `game_id`. Admin / system rows are not
// included.
table SentRequest {
game_id:common.UUID (required);
}
// SentResponse stores the caller's outgoing personal-message list.
// Each `MailMessage` carries the original recipient snapshot.
table SentResponse {
items:[MailMessage];
}
// MessageGetRequest stores the read-side request for a single
// message detail. The caller must be a recipient of the message.
table MessageGetRequest {
game_id:common.UUID (required);
message_id:common.UUID (required);
}
// MessageGetResponse stores the fully decorated message detail
// including any cached translation into the caller's preferred
// language.
table MessageGetResponse {
message:MailMessage;
}
// SendRequest stores the write-side request for a single-recipient
// personal send. Exactly one of `recipient_user_id` /
// `recipient_race_name` must be supplied; the empty string means
// "use the other field".
table SendRequest {
game_id:common.UUID (required);
recipient_user_id:string;
recipient_race_name:string;
subject:string;
body:string;
}
// SendResponse echoes the freshly inserted message detail.
table SendResponse {
message:MailMessage;
}
// BroadcastRequest stores the paid-tier player broadcast. The
// recipient set is always "every other active member of the game".
table BroadcastRequest {
game_id:common.UUID (required);
subject:string;
body:string;
}
// BroadcastResponse stores the receipt returned by the server.
table BroadcastResponse {
receipt:MailBroadcastReceipt;
}
// AdminRequest stores the owner-only admin send. `target="user"`
// requires exactly one of `recipient_user_id` / `recipient_race_name`;
// `target="all"` accepts the optional `recipients` scope (default
// `active`).
table AdminRequest {
game_id:common.UUID (required);
target:string;
recipient_user_id:string;
recipient_race_name:string;
recipients:string;
subject:string;
body:string;
}
// AdminResponse carries the result of an admin send. When the
// request had `target="user"`, `message` is set; when `target="all"`,
// `receipt` is set. Callers branch on which field is present.
table AdminResponse {
message:MailMessage;
receipt:MailBroadcastReceipt;
}
// ReadRequest stores the mark-read intent for a single message. The
// caller must be a recipient. Idempotent.
table ReadRequest {
game_id:common.UUID (required);
message_id:common.UUID (required);
}
// ReadResponse echoes the recipient state after the operation.
table ReadResponse {
state:MailRecipientState;
}
// DeleteRequest stores the soft-delete intent for a single message.
// The message must already be marked read (HTTP 409 otherwise).
table DeleteRequest {
game_id:common.UUID (required);
message_id:common.UUID (required);
}
// DeleteResponse echoes the recipient state after the operation.
table DeleteResponse {
state:MailRecipientState;
}
+133
View File
@@ -0,0 +1,133 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type AdminRequest struct {
_tab flatbuffers.Table
}
func GetRootAsAdminRequest(buf []byte, offset flatbuffers.UOffsetT) *AdminRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &AdminRequest{}
x.Init(buf, n+offset)
return x
}
func FinishAdminRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsAdminRequest(buf []byte, offset flatbuffers.UOffsetT) *AdminRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &AdminRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedAdminRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *AdminRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *AdminRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *AdminRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *AdminRequest) Target() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AdminRequest) RecipientUserId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AdminRequest) RecipientRaceName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AdminRequest) Recipients() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AdminRequest) Subject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *AdminRequest) Body() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func AdminRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(7)
}
func AdminRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func AdminRequestAddTarget(builder *flatbuffers.Builder, target flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(target), 0)
}
func AdminRequestAddRecipientUserId(builder *flatbuffers.Builder, recipientUserId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(recipientUserId), 0)
}
func AdminRequestAddRecipientRaceName(builder *flatbuffers.Builder, recipientRaceName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(recipientRaceName), 0)
}
func AdminRequestAddRecipients(builder *flatbuffers.Builder, recipients flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(recipients), 0)
}
func AdminRequestAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(subject), 0)
}
func AdminRequestAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(body), 0)
}
func AdminRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+81
View File
@@ -0,0 +1,81 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type AdminResponse struct {
_tab flatbuffers.Table
}
func GetRootAsAdminResponse(buf []byte, offset flatbuffers.UOffsetT) *AdminResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &AdminResponse{}
x.Init(buf, n+offset)
return x
}
func FinishAdminResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsAdminResponse(buf []byte, offset flatbuffers.UOffsetT) *AdminResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &AdminResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedAdminResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *AdminResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *AdminResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *AdminResponse) Message(obj *MailMessage) *MailMessage {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailMessage)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *AdminResponse) Receipt(obj *MailBroadcastReceipt) *MailBroadcastReceipt {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailBroadcastReceipt)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func AdminResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func AdminResponseAddMessage(builder *flatbuffers.Builder, message flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(message), 0)
}
func AdminResponseAddReceipt(builder *flatbuffers.Builder, receipt flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(receipt), 0)
}
func AdminResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,89 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type BroadcastRequest struct {
_tab flatbuffers.Table
}
func GetRootAsBroadcastRequest(buf []byte, offset flatbuffers.UOffsetT) *BroadcastRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &BroadcastRequest{}
x.Init(buf, n+offset)
return x
}
func FinishBroadcastRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsBroadcastRequest(buf []byte, offset flatbuffers.UOffsetT) *BroadcastRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &BroadcastRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedBroadcastRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *BroadcastRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *BroadcastRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *BroadcastRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *BroadcastRequest) Subject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *BroadcastRequest) Body() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func BroadcastRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func BroadcastRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func BroadcastRequestAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(subject), 0)
}
func BroadcastRequestAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(body), 0)
}
func BroadcastRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,65 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type BroadcastResponse struct {
_tab flatbuffers.Table
}
func GetRootAsBroadcastResponse(buf []byte, offset flatbuffers.UOffsetT) *BroadcastResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &BroadcastResponse{}
x.Init(buf, n+offset)
return x
}
func FinishBroadcastResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsBroadcastResponse(buf []byte, offset flatbuffers.UOffsetT) *BroadcastResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &BroadcastResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedBroadcastResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *BroadcastResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *BroadcastResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *BroadcastResponse) Receipt(obj *MailBroadcastReceipt) *MailBroadcastReceipt {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailBroadcastReceipt)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func BroadcastResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func BroadcastResponseAddReceipt(builder *flatbuffers.Builder, receipt flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(receipt), 0)
}
func BroadcastResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+83
View File
@@ -0,0 +1,83 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type DeleteRequest struct {
_tab flatbuffers.Table
}
func GetRootAsDeleteRequest(buf []byte, offset flatbuffers.UOffsetT) *DeleteRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &DeleteRequest{}
x.Init(buf, n+offset)
return x
}
func FinishDeleteRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsDeleteRequest(buf []byte, offset flatbuffers.UOffsetT) *DeleteRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &DeleteRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedDeleteRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *DeleteRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *DeleteRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *DeleteRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *DeleteRequest) MessageId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func DeleteRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func DeleteRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func DeleteRequestAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependStructSlot(1, flatbuffers.UOffsetT(messageId), 0)
}
func DeleteRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,65 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type DeleteResponse struct {
_tab flatbuffers.Table
}
func GetRootAsDeleteResponse(buf []byte, offset flatbuffers.UOffsetT) *DeleteResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &DeleteResponse{}
x.Init(buf, n+offset)
return x
}
func FinishDeleteResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsDeleteResponse(buf []byte, offset flatbuffers.UOffsetT) *DeleteResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &DeleteResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedDeleteResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *DeleteResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *DeleteResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *DeleteResponse) State(obj *MailRecipientState) *MailRecipientState {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailRecipientState)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func DeleteResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func DeleteResponseAddState(builder *flatbuffers.Builder, state flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(state), 0)
}
func DeleteResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+67
View File
@@ -0,0 +1,67 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type InboxRequest struct {
_tab flatbuffers.Table
}
func GetRootAsInboxRequest(buf []byte, offset flatbuffers.UOffsetT) *InboxRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &InboxRequest{}
x.Init(buf, n+offset)
return x
}
func FinishInboxRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsInboxRequest(buf []byte, offset flatbuffers.UOffsetT) *InboxRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &InboxRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedInboxRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *InboxRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *InboxRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *InboxRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func InboxRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func InboxRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func InboxRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+75
View File
@@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type InboxResponse struct {
_tab flatbuffers.Table
}
func GetRootAsInboxResponse(buf []byte, offset flatbuffers.UOffsetT) *InboxResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &InboxResponse{}
x.Init(buf, n+offset)
return x
}
func FinishInboxResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsInboxResponse(buf []byte, offset flatbuffers.UOffsetT) *InboxResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &InboxResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedInboxResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *InboxResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *InboxResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *InboxResponse) Items(obj *MailMessage, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *InboxResponse) ItemsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func InboxResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func InboxResponseAddItems(builder *flatbuffers.Builder, items flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(items), 0)
}
func InboxResponseStartItemsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func InboxResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,178 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MailBroadcastReceipt struct {
_tab flatbuffers.Table
}
func GetRootAsMailBroadcastReceipt(buf []byte, offset flatbuffers.UOffsetT) *MailBroadcastReceipt {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MailBroadcastReceipt{}
x.Init(buf, n+offset)
return x
}
func FinishMailBroadcastReceiptBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsMailBroadcastReceipt(buf []byte, offset flatbuffers.UOffsetT) *MailBroadcastReceipt {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MailBroadcastReceipt{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedMailBroadcastReceiptBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *MailBroadcastReceipt) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MailBroadcastReceipt) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MailBroadcastReceipt) MessageId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) GameName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) Kind() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) SenderKind() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) Subject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) Body() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) BodyLang() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) BroadcastScope() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailBroadcastReceipt) CreatedAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailBroadcastReceipt) MutateCreatedAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(22, n)
}
func (rcv *MailBroadcastReceipt) RecipientCount() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailBroadcastReceipt) MutateRecipientCount(n int32) bool {
return rcv._tab.MutateInt32Slot(24, n)
}
func MailBroadcastReceiptStart(builder *flatbuffers.Builder) {
builder.StartObject(11)
}
func MailBroadcastReceiptAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
}
func MailBroadcastReceiptAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(gameId), 0)
}
func MailBroadcastReceiptAddGameName(builder *flatbuffers.Builder, gameName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(gameName), 0)
}
func MailBroadcastReceiptAddKind(builder *flatbuffers.Builder, kind flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(kind), 0)
}
func MailBroadcastReceiptAddSenderKind(builder *flatbuffers.Builder, senderKind flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(senderKind), 0)
}
func MailBroadcastReceiptAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(subject), 0)
}
func MailBroadcastReceiptAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(body), 0)
}
func MailBroadcastReceiptAddBodyLang(builder *flatbuffers.Builder, bodyLang flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(bodyLang), 0)
}
func MailBroadcastReceiptAddBroadcastScope(builder *flatbuffers.Builder, broadcastScope flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(8, flatbuffers.UOffsetT(broadcastScope), 0)
}
func MailBroadcastReceiptAddCreatedAtMs(builder *flatbuffers.Builder, createdAtMs int64) {
builder.PrependInt64Slot(9, createdAtMs, 0)
}
func MailBroadcastReceiptAddRecipientCount(builder *flatbuffers.Builder, recipientCount int32) {
builder.PrependInt32Slot(10, recipientCount, 0)
}
func MailBroadcastReceiptEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+303
View File
@@ -0,0 +1,303 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MailMessage struct {
_tab flatbuffers.Table
}
func GetRootAsMailMessage(buf []byte, offset flatbuffers.UOffsetT) *MailMessage {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MailMessage{}
x.Init(buf, n+offset)
return x
}
func FinishMailMessageBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsMailMessage(buf []byte, offset flatbuffers.UOffsetT) *MailMessage {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MailMessage{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedMailMessageBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *MailMessage) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MailMessage) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MailMessage) MessageId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) GameId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) GameName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) Kind() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) SenderKind() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) SenderUserId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) SenderUsername() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) SenderRaceName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) Subject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) Body() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) BodyLang() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) BroadcastScope() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(26))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) CreatedAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(28))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailMessage) MutateCreatedAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(28, n)
}
func (rcv *MailMessage) RecipientUserId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) RecipientUserName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) RecipientRaceName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(34))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) ReadAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(36))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailMessage) MutateReadAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(36, n)
}
func (rcv *MailMessage) DeletedAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(38))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailMessage) MutateDeletedAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(38, n)
}
func (rcv *MailMessage) TranslatedSubject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(40))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) TranslatedBody() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(42))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) TranslationLang() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(44))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailMessage) Translator() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(46))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func MailMessageStart(builder *flatbuffers.Builder) {
builder.StartObject(22)
}
func MailMessageAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
}
func MailMessageAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(gameId), 0)
}
func MailMessageAddGameName(builder *flatbuffers.Builder, gameName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(gameName), 0)
}
func MailMessageAddKind(builder *flatbuffers.Builder, kind flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(kind), 0)
}
func MailMessageAddSenderKind(builder *flatbuffers.Builder, senderKind flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(senderKind), 0)
}
func MailMessageAddSenderUserId(builder *flatbuffers.Builder, senderUserId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(senderUserId), 0)
}
func MailMessageAddSenderUsername(builder *flatbuffers.Builder, senderUsername flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(senderUsername), 0)
}
func MailMessageAddSenderRaceName(builder *flatbuffers.Builder, senderRaceName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(senderRaceName), 0)
}
func MailMessageAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(8, flatbuffers.UOffsetT(subject), 0)
}
func MailMessageAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(9, flatbuffers.UOffsetT(body), 0)
}
func MailMessageAddBodyLang(builder *flatbuffers.Builder, bodyLang flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(10, flatbuffers.UOffsetT(bodyLang), 0)
}
func MailMessageAddBroadcastScope(builder *flatbuffers.Builder, broadcastScope flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(11, flatbuffers.UOffsetT(broadcastScope), 0)
}
func MailMessageAddCreatedAtMs(builder *flatbuffers.Builder, createdAtMs int64) {
builder.PrependInt64Slot(12, createdAtMs, 0)
}
func MailMessageAddRecipientUserId(builder *flatbuffers.Builder, recipientUserId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(recipientUserId), 0)
}
func MailMessageAddRecipientUserName(builder *flatbuffers.Builder, recipientUserName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(14, flatbuffers.UOffsetT(recipientUserName), 0)
}
func MailMessageAddRecipientRaceName(builder *flatbuffers.Builder, recipientRaceName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(15, flatbuffers.UOffsetT(recipientRaceName), 0)
}
func MailMessageAddReadAtMs(builder *flatbuffers.Builder, readAtMs int64) {
builder.PrependInt64Slot(16, readAtMs, 0)
}
func MailMessageAddDeletedAtMs(builder *flatbuffers.Builder, deletedAtMs int64) {
builder.PrependInt64Slot(17, deletedAtMs, 0)
}
func MailMessageAddTranslatedSubject(builder *flatbuffers.Builder, translatedSubject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(18, flatbuffers.UOffsetT(translatedSubject), 0)
}
func MailMessageAddTranslatedBody(builder *flatbuffers.Builder, translatedBody flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(19, flatbuffers.UOffsetT(translatedBody), 0)
}
func MailMessageAddTranslationLang(builder *flatbuffers.Builder, translationLang flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(20, flatbuffers.UOffsetT(translationLang), 0)
}
func MailMessageAddTranslator(builder *flatbuffers.Builder, translator flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(21, flatbuffers.UOffsetT(translator), 0)
}
func MailMessageEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,90 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MailRecipientState struct {
_tab flatbuffers.Table
}
func GetRootAsMailRecipientState(buf []byte, offset flatbuffers.UOffsetT) *MailRecipientState {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MailRecipientState{}
x.Init(buf, n+offset)
return x
}
func FinishMailRecipientStateBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsMailRecipientState(buf []byte, offset flatbuffers.UOffsetT) *MailRecipientState {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MailRecipientState{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedMailRecipientStateBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *MailRecipientState) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MailRecipientState) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MailRecipientState) MessageId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *MailRecipientState) ReadAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailRecipientState) MutateReadAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(6, n)
}
func (rcv *MailRecipientState) DeletedAtMs() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *MailRecipientState) MutateDeletedAtMs(n int64) bool {
return rcv._tab.MutateInt64Slot(8, n)
}
func MailRecipientStateStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func MailRecipientStateAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
}
func MailRecipientStateAddReadAtMs(builder *flatbuffers.Builder, readAtMs int64) {
builder.PrependInt64Slot(1, readAtMs, 0)
}
func MailRecipientStateAddDeletedAtMs(builder *flatbuffers.Builder, deletedAtMs int64) {
builder.PrependInt64Slot(2, deletedAtMs, 0)
}
func MailRecipientStateEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,83 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type MessageGetRequest struct {
_tab flatbuffers.Table
}
func GetRootAsMessageGetRequest(buf []byte, offset flatbuffers.UOffsetT) *MessageGetRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MessageGetRequest{}
x.Init(buf, n+offset)
return x
}
func FinishMessageGetRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsMessageGetRequest(buf []byte, offset flatbuffers.UOffsetT) *MessageGetRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MessageGetRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedMessageGetRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *MessageGetRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MessageGetRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MessageGetRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *MessageGetRequest) MessageId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func MessageGetRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func MessageGetRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func MessageGetRequestAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependStructSlot(1, flatbuffers.UOffsetT(messageId), 0)
}
func MessageGetRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
@@ -0,0 +1,65 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type MessageGetResponse struct {
_tab flatbuffers.Table
}
func GetRootAsMessageGetResponse(buf []byte, offset flatbuffers.UOffsetT) *MessageGetResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &MessageGetResponse{}
x.Init(buf, n+offset)
return x
}
func FinishMessageGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsMessageGetResponse(buf []byte, offset flatbuffers.UOffsetT) *MessageGetResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &MessageGetResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedMessageGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *MessageGetResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *MessageGetResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *MessageGetResponse) Message(obj *MailMessage) *MailMessage {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailMessage)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func MessageGetResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func MessageGetResponseAddMessage(builder *flatbuffers.Builder, message flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(message), 0)
}
func MessageGetResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+83
View File
@@ -0,0 +1,83 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type ReadRequest struct {
_tab flatbuffers.Table
}
func GetRootAsReadRequest(buf []byte, offset flatbuffers.UOffsetT) *ReadRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ReadRequest{}
x.Init(buf, n+offset)
return x
}
func FinishReadRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsReadRequest(buf []byte, offset flatbuffers.UOffsetT) *ReadRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ReadRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedReadRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ReadRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ReadRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ReadRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *ReadRequest) MessageId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func ReadRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func ReadRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func ReadRequestAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
builder.PrependStructSlot(1, flatbuffers.UOffsetT(messageId), 0)
}
func ReadRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+65
View File
@@ -0,0 +1,65 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type ReadResponse struct {
_tab flatbuffers.Table
}
func GetRootAsReadResponse(buf []byte, offset flatbuffers.UOffsetT) *ReadResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ReadResponse{}
x.Init(buf, n+offset)
return x
}
func FinishReadResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsReadResponse(buf []byte, offset flatbuffers.UOffsetT) *ReadResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ReadResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedReadResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ReadResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ReadResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ReadResponse) State(obj *MailRecipientState) *MailRecipientState {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailRecipientState)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func ReadResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func ReadResponseAddState(builder *flatbuffers.Builder, state flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(state), 0)
}
func ReadResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+111
View File
@@ -0,0 +1,111 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type SendRequest struct {
_tab flatbuffers.Table
}
func GetRootAsSendRequest(buf []byte, offset flatbuffers.UOffsetT) *SendRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &SendRequest{}
x.Init(buf, n+offset)
return x
}
func FinishSendRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsSendRequest(buf []byte, offset flatbuffers.UOffsetT) *SendRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &SendRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedSendRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *SendRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *SendRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *SendRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *SendRequest) RecipientUserId() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *SendRequest) RecipientRaceName() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *SendRequest) Subject() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *SendRequest) Body() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func SendRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(5)
}
func SendRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func SendRequestAddRecipientUserId(builder *flatbuffers.Builder, recipientUserId flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(recipientUserId), 0)
}
func SendRequestAddRecipientRaceName(builder *flatbuffers.Builder, recipientRaceName flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(recipientRaceName), 0)
}
func SendRequestAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(subject), 0)
}
func SendRequestAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(body), 0)
}
func SendRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+65
View File
@@ -0,0 +1,65 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type SendResponse struct {
_tab flatbuffers.Table
}
func GetRootAsSendResponse(buf []byte, offset flatbuffers.UOffsetT) *SendResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &SendResponse{}
x.Init(buf, n+offset)
return x
}
func FinishSendResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsSendResponse(buf []byte, offset flatbuffers.UOffsetT) *SendResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &SendResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedSendResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *SendResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *SendResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *SendResponse) Message(obj *MailMessage) *MailMessage {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Indirect(o + rcv._tab.Pos)
if obj == nil {
obj = new(MailMessage)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func SendResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func SendResponseAddMessage(builder *flatbuffers.Builder, message flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(message), 0)
}
func SendResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+67
View File
@@ -0,0 +1,67 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
common "galaxy/schema/fbs/common"
)
type SentRequest struct {
_tab flatbuffers.Table
}
func GetRootAsSentRequest(buf []byte, offset flatbuffers.UOffsetT) *SentRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &SentRequest{}
x.Init(buf, n+offset)
return x
}
func FinishSentRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsSentRequest(buf []byte, offset flatbuffers.UOffsetT) *SentRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &SentRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedSentRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *SentRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *SentRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *SentRequest) GameId(obj *common.UUID) *common.UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(common.UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func SentRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func SentRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func SentRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+75
View File
@@ -0,0 +1,75 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package diplomail
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type SentResponse struct {
_tab flatbuffers.Table
}
func GetRootAsSentResponse(buf []byte, offset flatbuffers.UOffsetT) *SentResponse {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &SentResponse{}
x.Init(buf, n+offset)
return x
}
func FinishSentResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsSentResponse(buf []byte, offset flatbuffers.UOffsetT) *SentResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &SentResponse{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedSentResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *SentResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *SentResponse) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *SentResponse) Items(obj *MailMessage, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *SentResponse) ItemsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func SentResponseStart(builder *flatbuffers.Builder) {
builder.StartObject(1)
}
func SentResponseAddItems(builder *flatbuffers.Builder, items flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(items), 0)
}
func SentResponseStartItemsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func SentResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+1 -1
View File
@@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm
WASM_EXEC := frontend/static/wasm_exec.js
TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null)
FBS_OUT := frontend/src/proto/galaxy/fbs
FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs
FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs ../pkg/schema/fbs/diplomail.fbs
help:
@echo "ui targets:"
+66 -2
View File
@@ -3070,9 +3070,73 @@ bottom):
- animated transitions when survivors re-distribute after an
elimination (currently hard-jumps).
## Phase 28. Diplomatic Mail View
## ~~Phase 28. Diplomatic Mail View~~
Status: pending.
Status: done (CI gate passed on run 136 — go-unit / ui-test / integration all green at commit 6d0272b).
Decisions baked in during implementation:
1. **Transport: ConnectRPC `user.games.mail.*`.** Eight new
authenticated commands (inbox / sent / message.get / send /
broadcast / admin / read / delete) plumbed end-to-end through
the existing gateway → backend REST surface. Schemas in
`pkg/schema/fbs/diplomail.fbs`; constants in
`pkg/model/diplomail/diplomail.go`; gateway translation in
`gateway/internal/backendclient/mail_commands.go`.
2. **Recipient by race name.** The send / admin endpoints accept
an alternative `recipient_race_name` field; backend resolves it
via `Memberships.ListMembers(gameID, "active")`. The UI feeds
the picker straight off `report.races[].name` — no client-side
memberships RPC.
3. **`sender_race_name` snapshot.** New nullable column on
`diplomail_messages`, populated for `sender_kind='player'`
senders that have an active membership at send time. Drives the
per-race threading on the client.
4. **/sent returns full message detail.** Backend's bulk sent
listing now returns the same `UserMailMessageDetail` shape as
`/inbox`, one row per (message, recipient). The UI collapses
broadcasts by `message_id` into a single stand-alone item.
5. **Threading + stand-alones.** `MailStore.entries` groups
personal messages by the other party's race name. System,
admin, and outgoing broadcasts render as stand-alone items in
the same list pane.
6. **No read receipts.** `read_at` and `deleted_at` drive the
badge counter and soft-delete affordance but are never shown
to the user.
7. **Header badge.** Inline pill on the view-menu "diplomatic
mail" row, fed by `mailStore.unreadCount`. No always-visible
chrome added.
8. **Push event reuse.** A new
`eventStream.on("diplomail.message.received", …)` handler in
`routes/games/[id]/+layout.svelte` parses the verified payload,
refreshes the inbox, and raises a `toast.show` with a "view"
deep-link.
9. **Translation toggle.** Per-message Show original / Show
translation toggle inside both `thread-pane.svelte` and
`system-item-pane.svelte`; the body defaults to the cached
translation when present.
Artifacts (delivered):
- backend: `internal/postgres/migrations/00001_init.sql`,
`internal/diplomail/{types.go,store.go,service.go,admin_send.go,diplomail_e2e_test.go,README.md}`,
`internal/server/{handlers_user_mail.go,handlers_admin_diplomail.go}`,
`openapi.yaml`;
- wire: `pkg/schema/fbs/diplomail.fbs` + generated Go and TS
bindings; `pkg/model/diplomail/diplomail.go`;
- gateway: `gateway/internal/backendclient/{mail_commands.go,routes.go,mail_commands_test.go}`,
`gateway/cmd/gateway/main.go`;
- ui: `ui/frontend/src/api/diplomail.ts`,
`ui/frontend/src/lib/mail-store.svelte.ts`,
`ui/frontend/src/lib/active-view/mail.svelte` (+ subdir
`mail/{thread-list,thread-pane,system-item-pane,compose,system-titles}.svelte|.ts`),
`ui/frontend/src/lib/header/view-menu.svelte`,
`ui/frontend/src/routes/games/[id]/+layout.svelte`,
`ui/frontend/src/lib/i18n/locales/{en,ru}.ts`;
- docs: `ui/docs/diplomail-ui.md`, `docs/FUNCTIONAL.md` §11.4 +
mirror in `docs/FUNCTIONAL_ru.md`.
Original phase brief follows.
Goal: implement a mail inbox and compose flow as a dedicated view that
replaces the map.
+97
View File
@@ -0,0 +1,97 @@
# In-game diplomatic mail UI
Phase 28 wires the in-game mail view that consumes the `diplomail`
subsystem in the backend. The route lives at `/games/:id/mail`
(registered in Phase 10) and replaces the active view when the user
opens the "diplomatic mail" entry in the header menu.
## Wire surface
Eight ConnectRPC commands sit between UI and backend, all under the
`user.games.mail.*` namespace:
| Command | Backend REST endpoint |
|---|---|
| `user.games.mail.inbox` | `GET /api/v1/user/games/{id}/mail/inbox` |
| `user.games.mail.sent` | `GET …/mail/sent` |
| `user.games.mail.message.get` | `GET …/mail/messages/{message_id}` |
| `user.games.mail.send` | `POST …/mail/messages` |
| `user.games.mail.broadcast` | `POST …/mail/broadcast` |
| `user.games.mail.admin` | `POST …/mail/admin` |
| `user.games.mail.read` | `POST …/mail/messages/{id}/read` |
| `user.games.mail.delete` | `DELETE …/mail/messages/{id}` |
The FlatBuffers schemas live under
[`pkg/schema/fbs/diplomail.fbs`](../../pkg/schema/fbs/diplomail.fbs);
the gateway translation lives in
[`gateway/internal/backendclient/mail_commands.go`](../../gateway/internal/backendclient/mail_commands.go).
## Recipient by race name
The compose flow does **not** consult a memberships listing. The
recipient picker reads `gameState.report.races[].name` (the Phase 22
projection of `report.player[]`), and the send request carries the
chosen race name as `recipient_race_name`. The backend resolves it
against `Memberships.ListMembers(gameID, "active")` and rejects with
`forbidden` if the matching member is no longer active. This keeps
the UI off the lobby surface for the common case.
## Threading model
`MailStore.entries` is the derived rune the active view consumes. It
projects the union of inbox and sent into:
- **Per-race threads** — every personal message keyed by another
race contributes to a thread keyed on that race name. Incoming is
keyed on `sender_race_name`; outgoing is keyed on
`recipient_race_name`. Thread messages are sorted oldest → newest
for chat-style rendering; the unread badge counts incoming
`read_at === null` rows only.
- **Stand-alone items** — system mail (`sender_kind=system`), admin
notifications (`sender_kind=admin`), and the caller's own
paid-tier broadcasts (`broadcast_scope=game_broadcast`). Backend
returns one row per recipient for paid-tier broadcasts; the UI
collapses them by `message_id` into a single stand-alone item.
`read_at` and `deleted_at` are not surfaced to the user in any pane
— they only drive the badge counter and the optimistic mark-read
state. This is intentional (per Phase 28 decisions): the user-facing
spec for diplomatic mail does not promise read receipts.
## Translation toggle
When a message detail carries `translated_body`, the body and (if
non-empty) subject default to the translated rendering. Each message
pane exposes a "Show original" / "Show translation" button that
flips the per-message state. Messages without a cached translation
render the original directly with no toggle.
## Push events
`diplomail.message.received` push frames are dispatched from
`api/events.svelte.ts` via the singleton SubscribeEvents stream. The
in-game layout (`routes/games/[id]/+layout.svelte`) parses the
verified payload, calls `mailStore.applyPushEvent(gameId)` (which
re-fetches the inbox — the payload only carries a preview), and
raises a toast through `lib/toast.svelte.ts` with a "view"
deep-link to `/games/:id/mail`.
The header view-menu's mail entry shows `mailStore.unreadCount` as
an inline pill — the only chrome the badge needs.
## Layout
Desktop (≥ 768 px) renders a two-pane CSS grid: list on the left,
detail on the right. Mobile flips to a single-pane stack; tapping a
list row hides the list and shows the detail with a back button.
## Accessibility
- Bodies render through Svelte's default text-content path (no HTML
parsing) per the backend rule of treating message text as plain
UTF-8.
- The compose dialog uses native form controls; the recipient
picker is a `<select>` so screen-readers and keyboard users get
the standard semantics.
- The reply box and the compose body are real `<textarea>`s so
shift-enter newlines, paste, and selection behave correctly.
+421
View File
@@ -0,0 +1,421 @@
// Typed wrappers around `GalaxyClient.executeCommand` for the eight
// `user.games.mail.*` Phase 28 ConnectRPC commands. Each wrapper
// builds the matching FlatBuffers request, decodes the FlatBuffers
// response, and surfaces backend errors through `MailError` so callers
// branch on canonical codes (`invalid_request`, `forbidden`,
// `not_found`, `conflict`).
import { Builder, ByteBuffer } from "flatbuffers";
import type { GalaxyClient } from "./galaxy-client";
import { uuidToHiLo } from "./game-state";
import {
AdminRequest,
AdminResponse,
BroadcastRequest,
BroadcastResponse,
DeleteRequest,
DeleteResponse,
InboxRequest,
InboxResponse,
MailMessage as FbsMailMessage,
MailRecipientState as FbsMailRecipientState,
MailBroadcastReceipt as FbsMailBroadcastReceipt,
MessageGetRequest,
MessageGetResponse,
ReadRequest,
ReadResponse,
SendRequest,
SendResponse,
SentRequest,
SentResponse,
} from "../proto/galaxy/fbs/diplomail";
import { UUID } from "../proto/galaxy/fbs/common";
import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby";
/**
* MailError represents a non-`ok` response from a mail RPC. Callers
* branch on `code` for canonical error handling and use `message` for
* inline UI surfacing.
*/
export class MailError extends Error {
readonly resultCode: string;
readonly code: string;
constructor(resultCode: string, code: string, message: string) {
super(message);
this.name = "MailError";
this.resultCode = resultCode;
this.code = code;
}
}
/**
* MailMessage is the typed UI view of a `MailMessage` FlatBuffers row.
* Nullable wire fields (`sender_user_id`, timestamps, translation
* slots) become `null` here; the empty string from FB readers is
* normalised to either `""` or `null` based on field semantics.
*/
export interface MailMessage {
messageId: string;
gameId: string;
gameName: string;
kind: string;
senderKind: string;
senderUserId: string | null;
senderUsername: string | null;
senderRaceName: string | null;
subject: string;
body: string;
bodyLang: string;
broadcastScope: string;
createdAt: Date;
recipientUserId: string;
recipientUserName: string;
recipientRaceName: string | null;
readAt: Date | null;
deletedAt: Date | null;
translatedSubject: string | null;
translatedBody: string | null;
translationLang: string | null;
translator: string | null;
}
export interface MailRecipientState {
messageId: string;
readAt: Date | null;
deletedAt: Date | null;
}
export interface MailBroadcastReceipt {
messageId: string;
gameId: string;
gameName: string;
kind: string;
senderKind: string;
subject: string;
body: string;
bodyLang: string;
broadcastScope: string;
createdAt: Date;
recipientCount: number;
}
export interface SendPersonalArgs {
gameId: string;
raceName: string;
subject?: string;
body: string;
}
export interface SendBroadcastArgs {
gameId: string;
subject?: string;
body: string;
}
export type AdminTarget = "user" | "all";
export interface SendAdminArgs {
gameId: string;
target: AdminTarget;
raceName?: string;
recipientUserId?: string;
recipients?: string;
subject?: string;
body: string;
}
const MESSAGE_TYPE_INBOX = "user.games.mail.inbox";
const MESSAGE_TYPE_SENT = "user.games.mail.sent";
const MESSAGE_TYPE_GET = "user.games.mail.message.get";
const MESSAGE_TYPE_SEND = "user.games.mail.send";
const MESSAGE_TYPE_BROADCAST = "user.games.mail.broadcast";
const MESSAGE_TYPE_ADMIN = "user.games.mail.admin";
const MESSAGE_TYPE_READ = "user.games.mail.read";
const MESSAGE_TYPE_DELETE = "user.games.mail.delete";
const RESULT_CODE_OK = "ok";
export async function fetchInbox(
client: GalaxyClient,
gameId: string,
): Promise<MailMessage[]> {
const builder = new Builder(64);
const [hi, lo] = uuidToHiLo(gameId);
InboxRequest.startInboxRequest(builder);
InboxRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
builder.finish(InboxRequest.endInboxRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_INBOX, builder.asUint8Array());
const response = InboxResponse.getRootAsInboxResponse(new ByteBuffer(payload));
return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
}
export async function fetchSent(
client: GalaxyClient,
gameId: string,
): Promise<MailMessage[]> {
const builder = new Builder(64);
const [hi, lo] = uuidToHiLo(gameId);
SentRequest.startSentRequest(builder);
SentRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
builder.finish(SentRequest.endSentRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_SENT, builder.asUint8Array());
const response = SentResponse.getRootAsSentResponse(new ByteBuffer(payload));
return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
}
export async function fetchMessage(
client: GalaxyClient,
gameId: string,
messageId: string,
): Promise<MailMessage> {
const builder = new Builder(64);
const [ghi, glo] = uuidToHiLo(gameId);
const [mhi, mlo] = uuidToHiLo(messageId);
MessageGetRequest.startMessageGetRequest(builder);
MessageGetRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
MessageGetRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
builder.finish(MessageGetRequest.endMessageGetRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_GET, builder.asUint8Array());
const response = MessageGetResponse.getRootAsMessageGetResponse(new ByteBuffer(payload));
const fb = response.message();
if (fb === null) {
throw new MailError("internal_error", "internal_error", "message missing in response");
}
return decodeMailMessage(fb);
}
export async function sendPersonal(
client: GalaxyClient,
input: SendPersonalArgs,
): Promise<MailMessage> {
const builder = new Builder(256);
const [hi, lo] = uuidToHiLo(input.gameId);
const raceOff = builder.createString(input.raceName);
const subjectOff = builder.createString(input.subject ?? "");
const bodyOff = builder.createString(input.body);
SendRequest.startSendRequest(builder);
SendRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
SendRequest.addRecipientRaceName(builder, raceOff);
SendRequest.addSubject(builder, subjectOff);
SendRequest.addBody(builder, bodyOff);
builder.finish(SendRequest.endSendRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_SEND, builder.asUint8Array());
const response = SendResponse.getRootAsSendResponse(new ByteBuffer(payload));
const fb = response.message();
if (fb === null) {
throw new MailError("internal_error", "internal_error", "message missing in response");
}
return decodeMailMessage(fb);
}
export async function sendBroadcast(
client: GalaxyClient,
input: SendBroadcastArgs,
): Promise<MailBroadcastReceipt> {
const builder = new Builder(256);
const [hi, lo] = uuidToHiLo(input.gameId);
const subjectOff = builder.createString(input.subject ?? "");
const bodyOff = builder.createString(input.body);
BroadcastRequest.startBroadcastRequest(builder);
BroadcastRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
BroadcastRequest.addSubject(builder, subjectOff);
BroadcastRequest.addBody(builder, bodyOff);
builder.finish(BroadcastRequest.endBroadcastRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_BROADCAST, builder.asUint8Array());
const response = BroadcastResponse.getRootAsBroadcastResponse(new ByteBuffer(payload));
const fb = response.receipt();
if (fb === null) {
throw new MailError("internal_error", "internal_error", "receipt missing in response");
}
return decodeMailBroadcastReceipt(fb);
}
export async function sendAdmin(
client: GalaxyClient,
input: SendAdminArgs,
): Promise<MailMessage | MailBroadcastReceipt> {
const builder = new Builder(256);
const [hi, lo] = uuidToHiLo(input.gameId);
const targetOff = builder.createString(input.target);
const recipientUserOff = builder.createString(input.recipientUserId ?? "");
const recipientRaceOff = builder.createString(input.raceName ?? "");
const recipientsOff = builder.createString(input.recipients ?? "");
const subjectOff = builder.createString(input.subject ?? "");
const bodyOff = builder.createString(input.body);
AdminRequest.startAdminRequest(builder);
AdminRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
AdminRequest.addTarget(builder, targetOff);
AdminRequest.addRecipientUserId(builder, recipientUserOff);
AdminRequest.addRecipientRaceName(builder, recipientRaceOff);
AdminRequest.addRecipients(builder, recipientsOff);
AdminRequest.addSubject(builder, subjectOff);
AdminRequest.addBody(builder, bodyOff);
builder.finish(AdminRequest.endAdminRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_ADMIN, builder.asUint8Array());
const response = AdminResponse.getRootAsAdminResponse(new ByteBuffer(payload));
const receipt = response.receipt();
if (receipt !== null) {
return decodeMailBroadcastReceipt(receipt);
}
const message = response.message();
if (message !== null) {
return decodeMailMessage(message);
}
throw new MailError("internal_error", "internal_error", "admin response carried neither message nor receipt");
}
export async function markRead(
client: GalaxyClient,
gameId: string,
messageId: string,
): Promise<MailRecipientState> {
const builder = new Builder(64);
const [ghi, glo] = uuidToHiLo(gameId);
const [mhi, mlo] = uuidToHiLo(messageId);
ReadRequest.startReadRequest(builder);
ReadRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
ReadRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
builder.finish(ReadRequest.endReadRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_READ, builder.asUint8Array());
const response = ReadResponse.getRootAsReadResponse(new ByteBuffer(payload));
const fb = response.state();
if (fb === null) {
throw new MailError("internal_error", "internal_error", "state missing in response");
}
return decodeMailRecipientState(fb);
}
export async function deleteMessage(
client: GalaxyClient,
gameId: string,
messageId: string,
): Promise<MailRecipientState> {
const builder = new Builder(64);
const [ghi, glo] = uuidToHiLo(gameId);
const [mhi, mlo] = uuidToHiLo(messageId);
DeleteRequest.startDeleteRequest(builder);
DeleteRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
DeleteRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
builder.finish(DeleteRequest.endDeleteRequest(builder));
const payload = await execute(client, MESSAGE_TYPE_DELETE, builder.asUint8Array());
const response = DeleteResponse.getRootAsDeleteResponse(new ByteBuffer(payload));
const fb = response.state();
if (fb === null) {
throw new MailError("internal_error", "internal_error", "state missing in response");
}
return decodeMailRecipientState(fb);
}
async function execute(
client: GalaxyClient,
messageType: string,
payloadBytes: Uint8Array,
): Promise<Uint8Array> {
const result = await client.executeCommand(messageType, payloadBytes);
if (result.resultCode !== RESULT_CODE_OK) {
throw decodeMailError(result.resultCode, result.payloadBytes);
}
return result.payloadBytes;
}
function decodeMailError(resultCode: string, payload: Uint8Array): MailError {
let code = resultCode;
let message = resultCode;
try {
const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload));
const body = errorResponse.error();
if (body) {
code = body.code() ?? resultCode;
message = body.message() ?? resultCode;
}
} catch (_err) {
// fall through to use raw resultCode
}
return new MailError(resultCode, code, message);
}
function readMessageList(
lengthFn: () => number,
getFn: (i: number) => FbsMailMessage | null,
): MailMessage[] {
const total = lengthFn();
const out: MailMessage[] = [];
for (let i = 0; i < total; i++) {
const item = getFn(i);
if (item) {
out.push(decodeMailMessage(item));
}
}
return out;
}
function decodeMailMessage(fb: FbsMailMessage): MailMessage {
return {
messageId: fb.messageId() ?? "",
gameId: fb.gameId() ?? "",
gameName: fb.gameName() ?? "",
kind: fb.kind() ?? "",
senderKind: fb.senderKind() ?? "",
senderUserId: optionalString(fb.senderUserId()),
senderUsername: optionalString(fb.senderUsername()),
senderRaceName: optionalString(fb.senderRaceName()),
subject: fb.subject() ?? "",
body: fb.body() ?? "",
bodyLang: fb.bodyLang() ?? "",
broadcastScope: fb.broadcastScope() ?? "",
createdAt: dateFromMs(fb.createdAtMs()),
recipientUserId: fb.recipientUserId() ?? "",
recipientUserName: fb.recipientUserName() ?? "",
recipientRaceName: optionalString(fb.recipientRaceName()),
readAt: optionalDateFromMs(fb.readAtMs()),
deletedAt: optionalDateFromMs(fb.deletedAtMs()),
translatedSubject: optionalString(fb.translatedSubject()),
translatedBody: optionalString(fb.translatedBody()),
translationLang: optionalString(fb.translationLang()),
translator: optionalString(fb.translator()),
};
}
function decodeMailRecipientState(fb: FbsMailRecipientState): MailRecipientState {
return {
messageId: fb.messageId() ?? "",
readAt: optionalDateFromMs(fb.readAtMs()),
deletedAt: optionalDateFromMs(fb.deletedAtMs()),
};
}
function decodeMailBroadcastReceipt(fb: FbsMailBroadcastReceipt): MailBroadcastReceipt {
return {
messageId: fb.messageId() ?? "",
gameId: fb.gameId() ?? "",
gameName: fb.gameName() ?? "",
kind: fb.kind() ?? "",
senderKind: fb.senderKind() ?? "",
subject: fb.subject() ?? "",
body: fb.body() ?? "",
bodyLang: fb.bodyLang() ?? "",
broadcastScope: fb.broadcastScope() ?? "",
createdAt: dateFromMs(fb.createdAtMs()),
recipientCount: fb.recipientCount(),
};
}
function optionalString(value: string | null | undefined): string | null {
if (value === null || value === undefined || value === "") {
return null;
}
return value;
}
function dateFromMs(ms: bigint): Date {
return new Date(Number(ms));
}
function optionalDateFromMs(ms: bigint): Date | null {
if (ms === 0n) {
return null;
}
return new Date(Number(ms));
}
+192 -12
View File
@@ -1,27 +1,207 @@
<!--
Phase 10 stub for the diplomatic-mail active view. Phase 28 wires the
real mail listing.
Phase 28 active-view for the diplomatic mail. Replaces the Phase 10
stub. Renders a two-pane list/detail layout on desktop and a
one-pane stack on mobile; the inner pieces (thread list, thread
pane, system-item pane, compose form) live under
`./mail/*.svelte`.
-->
<script lang="ts">
import { page } from "$app/state";
import { i18n } from "$lib/i18n/index.svelte";
import { mailStore, type MailListEntry } from "$lib/mail-store.svelte";
import ThreadList from "./mail/thread-list.svelte";
import ThreadPane from "./mail/thread-pane.svelte";
import SystemItemPane from "./mail/system-item-pane.svelte";
import Compose from "./mail/compose.svelte";
let selectedKey = $state<string | null>(null);
let composeOpen = $state(false);
const gameId = $derived(page.params.id ?? "");
const entries = $derived(mailStore.entries);
const selected = $derived.by<MailListEntry | null>(() => {
if (selectedKey === null) {
return null;
}
return entries.find((entry) => entryKey(entry) === selectedKey) ?? null;
});
function entryKey(entry: MailListEntry): string {
return entry.kind === "thread"
? `thread:${entry.raceName}`
: `standalone:${entry.message.messageId}`;
}
function openEntry(entry: MailListEntry): void {
selectedKey = entryKey(entry);
}
function closePane(): void {
selectedKey = null;
}
</script>
<section class="active-view" data-testid="active-view-mail">
<h2>{i18n.t("game.view.mail")}</h2>
<p>{i18n.t("game.shell.coming_soon")}</p>
<section class="mail" data-testid="active-view-mail">
<header class="mail-header">
<h2>{i18n.t("game.view.mail")}</h2>
<button
type="button"
class="compose-btn"
data-testid="mail-compose-open"
onclick={() => (composeOpen = true)}
disabled={mailStore.status !== "ready"}
>
{i18n.t("game.mail.compose_action")}
</button>
</header>
{#if mailStore.status === "loading"}
<p class="status" data-testid="mail-loading">
{i18n.t("game.mail.loading")}
</p>
{:else if mailStore.status === "error"}
<p class="status error" data-testid="mail-error">
{mailStore.error ?? i18n.t("game.mail.load_failed")}
</p>
{:else if entries.length === 0}
<p class="status" data-testid="mail-empty">
{i18n.t("game.mail.empty")}
</p>
{:else}
<div class="panes" class:detail-open={selected !== null}>
<div class="list-pane">
<ThreadList
{entries}
selectedKey={selectedKey}
onSelect={openEntry}
/>
</div>
<div class="detail-pane">
<button
type="button"
class="back-btn"
data-testid="mail-back"
onclick={closePane}
>
{i18n.t("game.mail.back")}
</button>
{#if selected === null}
<p class="status empty-detail">
{i18n.t("game.mail.select_thread")}
</p>
{:else if selected.kind === "thread"}
<ThreadPane thread={selected} {gameId} />
{:else}
<SystemItemPane entry={selected} />
{/if}
</div>
</div>
{/if}
{#if composeOpen}
<Compose
onClose={() => (composeOpen = false)}
onSent={(raceName: string | null) => {
composeOpen = false;
if (raceName !== null) {
selectedKey = `thread:${raceName}`;
}
}}
/>
{/if}
</section>
<style>
.active-view {
padding: 1.5rem;
.mail {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
font-family: system-ui, sans-serif;
}
.active-view h2 {
margin: 0 0 0.5rem;
.mail-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.mail-header h2 {
margin: 0;
font-size: 1.1rem;
}
.active-view p {
margin: 0;
color: #555;
.compose-btn {
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.compose-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
color: #888;
}
.status.error {
color: #c62828;
}
.panes {
display: grid;
grid-template-columns: minmax(220px, 280px) 1fr;
gap: 1rem;
min-height: 320px;
}
.list-pane,
.detail-pane {
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 0.75rem;
background: #111;
overflow: hidden;
}
.list-pane {
max-height: 70vh;
overflow-y: auto;
}
.back-btn {
display: none;
font: inherit;
margin-bottom: 0.5rem;
padding: 0.25rem 0.5rem;
border: 1px solid #444;
background: transparent;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.empty-detail {
text-align: center;
padding: 2rem 0;
}
@media (max-width: 767px) {
.panes {
grid-template-columns: 1fr;
}
.list-pane {
display: block;
}
.detail-pane {
display: none;
}
.panes.detail-open .list-pane {
display: none;
}
.panes.detail-open .detail-pane {
display: block;
}
.panes.detail-open .back-btn {
display: inline-block;
}
}
</style>
@@ -0,0 +1,273 @@
<!--
Phase 28 — compose dialog for diplomatic mail. The recipient picker
reads `gameState.report.races[]` (Phase 22), the kind toggle exposes
personal / broadcast / admin; broadcast and admin sends are gated
server-side, the UI surfaces the resulting 403 inline.
-->
<script lang="ts">
import { getContext } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import { mailStore } from "$lib/mail-store.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
type ComposeKind = "personal" | "broadcast" | "admin";
type AdminAudience = "active" | "active_and_removed" | "all_members";
let {
onClose,
onSent,
}: {
onClose: () => void;
onSent: (raceName: string | null) => void;
} = $props();
const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
);
const races = $derived.by<string[]>(() => {
const r = rendered?.report;
if (!r) {
return [];
}
return r.races.map((race) => race.name);
});
let kind = $state<ComposeKind>("personal");
let raceName = $state("");
let adminTarget = $state<"user" | "all">("user");
let adminAudience = $state<AdminAudience>("active");
let subject = $state("");
let body = $state("");
let error = $state<string | null>(null);
let sending = $state(false);
$effect(() => {
if (raceName === "" && races.length > 0) {
raceName = races[0];
}
});
async function submit(event: SubmitEvent): Promise<void> {
event.preventDefault();
error = null;
const bodyText = body.trim();
if (bodyText === "") {
error = i18n.t("game.mail.body_required");
return;
}
const needsRecipient = kind === "personal" || (kind === "admin" && adminTarget === "user");
if (needsRecipient && raceName === "") {
error = i18n.t("game.mail.recipient_required");
return;
}
sending = true;
try {
if (kind === "personal") {
await mailStore.composePersonal({
raceName,
subject,
body: bodyText,
});
onSent(raceName);
return;
}
if (kind === "broadcast") {
await mailStore.composeBroadcast({ subject, body: bodyText });
onSent(null);
return;
}
await mailStore.composeAdmin({
target: adminTarget,
raceName: adminTarget === "user" ? raceName : undefined,
recipients: adminTarget === "all" ? adminAudience : undefined,
subject,
body: bodyText,
});
onSent(null);
} catch (err) {
error = err instanceof Error ? err.message : String(err);
} finally {
sending = false;
}
}
</script>
<div class="overlay" data-testid="mail-compose">
<form class="dialog" onsubmit={submit}>
<header>
<h3>{i18n.t("game.mail.compose_action")}</h3>
<button type="button" class="close" onclick={onClose}>×</button>
</header>
<label>
{i18n.t("game.mail.compose.target_label")}
<select bind:value={kind} data-testid="mail-compose-kind">
<option value="personal">{i18n.t("game.mail.compose.target_personal")}</option>
<option value="broadcast">{i18n.t("game.mail.compose.target_broadcast")}</option>
<option value="admin">{i18n.t("game.mail.compose.target_admin")}</option>
</select>
</label>
{#if kind === "admin"}
<label>
{i18n.t("game.mail.compose.recipients_label")}
<select bind:value={adminTarget} data-testid="mail-compose-admin-target">
<option value="user">{i18n.t("game.mail.compose.target_personal")}</option>
<option value="all">{i18n.t("game.mail.compose.target_broadcast")}</option>
</select>
</label>
{#if adminTarget === "all"}
<label>
{i18n.t("game.mail.compose.recipients_label")}
<select bind:value={adminAudience} data-testid="mail-compose-admin-audience">
<option value="active">{i18n.t("game.mail.compose.recipients_active")}</option>
<option value="active_and_removed">{i18n.t("game.mail.compose.recipients_active_and_removed")}</option>
<option value="all_members">{i18n.t("game.mail.compose.recipients_all_members")}</option>
</select>
</label>
{/if}
{/if}
{#if kind === "personal" || (kind === "admin" && adminTarget === "user")}
<label>
{i18n.t("game.mail.recipient_label")}
<select bind:value={raceName} data-testid="mail-compose-recipient">
{#each races as race (race)}
<option value={race}>{race}</option>
{/each}
</select>
</label>
{/if}
<label>
<span class="visually-hidden">{i18n.t("game.mail.subject_placeholder")}</span>
<input
type="text"
bind:value={subject}
placeholder={i18n.t("game.mail.subject_placeholder")}
data-testid="mail-compose-subject"
/>
</label>
<label>
<span class="visually-hidden">{i18n.t("game.mail.body_placeholder")}</span>
<textarea
bind:value={body}
placeholder={i18n.t("game.mail.body_placeholder")}
rows="6"
data-testid="mail-compose-body"
></textarea>
</label>
{#if error}
<p class="error" data-testid="mail-compose-error">{error}</p>
{/if}
<footer>
<button type="button" onclick={onClose}>
{i18n.t("game.mail.compose.cancel")}
</button>
<button type="submit" disabled={sending} data-testid="mail-compose-send">
{i18n.t("game.mail.compose.send")}
</button>
</footer>
</form>
</div>
<style>
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 50;
}
.dialog {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.25rem;
background: #161616;
border: 1px solid #2a2a2a;
border-radius: 8px;
min-width: min(420px, 90vw);
max-width: min(560px, 95vw);
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
header h3 {
margin: 0;
font-size: 1rem;
}
.close {
font: inherit;
background: transparent;
border: none;
color: inherit;
font-size: 1.25rem;
cursor: pointer;
}
label {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.85rem;
color: #ccc;
}
input,
textarea,
select {
font: inherit;
padding: 0.4rem 0.5rem;
border: 1px solid #444;
background: #111;
color: inherit;
border-radius: 4px;
}
footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
footer button {
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
footer button[type="submit"] {
background: #2a4d7d;
border-color: #2a4d7d;
}
footer button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #c62828;
font-size: 0.85rem;
margin: 0;
}
.visually-hidden {
position: absolute;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
width: 1px;
overflow: hidden;
}
</style>
@@ -0,0 +1,106 @@
<!--
Phase 28 — right-pane for stand-alone messages (system mail, admin
notifications, and the caller's own paid-tier broadcasts). The pane
is read-only: no reply box, no per-recipient context. Soft-delete is
available for incoming rows that the caller has read.
-->
<script lang="ts">
import { onMount } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import { mailStore, type MailStandalone } from "$lib/mail-store.svelte";
import { systemTitleKey } from "./system-titles";
let { entry }: { entry: MailStandalone } = $props();
let showOriginal = $state(false);
const incoming = $derived(entry.message.recipientUserName !== "" && entry.message.senderKind !== "player");
onMount(() => {
if (incoming && entry.message.readAt === null) {
void mailStore.markRead(entry.message.messageId);
}
});
const displayBody = $derived(
entry.message.translatedBody && !showOriginal
? entry.message.translatedBody
: entry.message.body,
);
const displaySubject = $derived(
entry.message.translatedSubject && !showOriginal
? entry.message.translatedSubject
: entry.message.subject,
);
const headerKey = $derived.by(() => {
const m = entry.message;
if (m.senderKind === "system") {
return systemTitleKey(m);
}
if (m.senderKind === "admin") {
return "game.mail.admin.title" as const;
}
return "game.mail.broadcast.title" as const;
});
</script>
<div class="standalone" data-testid="mail-system-item">
<h3 class="title">{i18n.t(headerKey)}</h3>
{#if displaySubject}
<div class="subject">{displaySubject}</div>
{/if}
<p class="body">{displayBody}</p>
{#if entry.message.translatedBody}
<button
type="button"
class="toggle"
onclick={() => (showOriginal = !showOriginal)}
>
{showOriginal ? i18n.t("game.mail.show_translation") : i18n.t("game.mail.show_original")}
</button>
{/if}
{#if incoming}
<button
type="button"
class="delete"
onclick={() => mailStore.softDelete(entry.message.messageId)}
data-testid="mail-system-delete"
>
{i18n.t("game.mail.delete_action")}
</button>
{/if}
</div>
<style>
.standalone {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.title {
margin: 0;
font-size: 1rem;
color: #b3a14c;
}
.subject {
font-weight: 700;
}
.body {
margin: 0;
white-space: pre-wrap;
}
.toggle,
.delete {
align-self: flex-start;
font: inherit;
padding: 0.2rem 0.5rem;
border: 1px solid #444;
background: transparent;
color: inherit;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
</style>
@@ -0,0 +1,30 @@
// Maps a system-mail message (lifecycle hook) to its i18n title key.
// Kept as a typed helper so the thread-list and detail panes pick the
// same title even when the body templates evolve.
import type { TranslationKey } from "$lib/i18n/index.svelte";
import type { MailMessage } from "../../../api/diplomail";
const KEYWORDS: Array<{ test: RegExp; key: TranslationKey }> = [
{ test: /game[._ ]paused/i, key: "game.mail.system.game_paused.title" },
{ test: /game[._ ]cancelled|cancelled/i, key: "game.mail.system.game_cancelled.title" },
{ test: /membership[._ ]removed|kicked/i, key: "game.mail.system.membership_removed.title" },
{ test: /membership[._ ]blocked|blocked/i, key: "game.mail.system.membership_blocked.title" },
];
/**
* systemTitleKey returns the localised title key for a system mail
* row. The lobby renders these messages through templated subjects;
* the UI matches on the subject to pick a canonical title regardless
* of language. Falls back to a generic system-mail title when no
* pattern matches.
*/
export function systemTitleKey(message: MailMessage): TranslationKey {
const subject = message.subject ?? "";
for (const { test, key } of KEYWORDS) {
if (test.test(subject)) {
return key;
}
}
return "game.mail.system.generic.title";
}
@@ -0,0 +1,130 @@
<!--
Phase 28 — left-pane list of mail entries. Each entry is either a
per-race thread (collapsed to the latest message) or a stand-alone
system / admin / outgoing-broadcast item. The list is virtual only
inside its scroll container; PixiJS / canvas concerns do not apply
here.
-->
<script lang="ts">
import { i18n } from "$lib/i18n/index.svelte";
import type { MailListEntry } from "$lib/mail-store.svelte";
import { systemTitleKey } from "./system-titles";
let {
entries,
selectedKey,
onSelect,
}: {
entries: MailListEntry[];
selectedKey: string | null;
onSelect: (entry: MailListEntry) => void;
} = $props();
function entryKey(entry: MailListEntry): string {
return entry.kind === "thread"
? `thread:${entry.raceName}`
: `standalone:${entry.message.messageId}`;
}
function snippet(entry: MailListEntry): string {
if (entry.kind === "thread") {
const last = entry.messages[entry.messages.length - 1];
return last.subject || last.body;
}
return entry.message.subject || entry.message.body;
}
</script>
<ul class="list" data-testid="mail-thread-list">
{#each entries as entry (entryKey(entry))}
<li
class="row"
class:active={selectedKey === entryKey(entry)}
class:standalone={entry.kind === "standalone"}
class:has-unread={entry.kind === "thread" && entry.unreadCount > 0}
data-testid="mail-list-row"
>
<button
type="button"
class="row-btn"
onclick={() => onSelect(entry)}
data-thread-key={entryKey(entry)}
>
<span class="title">
{#if entry.kind === "thread"}
{entry.raceName}
{:else if entry.message.senderKind === "system"}
{i18n.t(systemTitleKey(entry.message))}
{:else if entry.message.senderKind === "admin"}
{i18n.t("game.mail.admin.title")}
{:else}
{i18n.t("game.mail.broadcast.title")}
{/if}
</span>
{#if entry.kind === "thread" && entry.unreadCount > 0}
<span class="badge" data-testid="mail-row-unread">{entry.unreadCount}</span>
{/if}
<span class="snippet">{snippet(entry)}</span>
</button>
</li>
{/each}
</ul>
<style>
.list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.row-btn {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 0.25rem 0.5rem;
text-align: left;
width: 100%;
padding: 0.5rem 0.75rem;
font: inherit;
background: transparent;
color: inherit;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
}
.row.active .row-btn {
border-color: #555;
background: #1c1c1c;
}
.row.has-unread .title {
font-weight: 700;
}
.row.standalone .title {
color: #b3a14c;
}
.title {
grid-column: 1 / span 1;
}
.badge {
grid-column: 2 / span 1;
grid-row: 1 / span 1;
justify-self: end;
min-width: 1.5rem;
padding: 0 0.4rem;
text-align: center;
font-size: 0.75rem;
border-radius: 999px;
background: #2a4d7d;
color: #fff;
}
.snippet {
grid-column: 1 / span 2;
color: #999;
font-size: 0.85rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -0,0 +1,255 @@
<!--
Phase 28 — right-pane transcript for a single per-race thread.
Renders messages oldest → newest, with outgoing messages visually
distinct from incoming. Each message body goes through `textContent`
(no HTML parsing); the optional translation has a per-message
"show original" / "show translation" toggle. A persistent reply box
sits at the bottom of the pane.
-->
<script lang="ts">
import { onMount } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import { mailStore, type MailThread } from "$lib/mail-store.svelte";
import type { MailMessage } from "../../../api/diplomail";
let {
thread,
gameId,
}: {
thread: MailThread;
gameId: string;
} = $props();
let replyBody = $state("");
let replyError = $state<string | null>(null);
let sending = $state(false);
const showOriginal = $state<Map<string, boolean>>(new Map());
// Mark every still-unread incoming message in this thread as read
// when the pane mounts. Idempotent on the server; the store
// optimistically flips `readAt` so the header badge updates
// without waiting for the round-trip.
onMount(() => {
for (const m of thread.messages) {
const incoming = m.senderRaceName === thread.raceName;
if (incoming && m.readAt === null) {
void mailStore.markRead(m.messageId);
}
}
});
function isOutgoing(m: MailMessage): boolean {
// Outgoing messages have the local user as sender, which
// corresponds to `recipientRaceName === thread.raceName` (the
// thread is keyed on the other party's race name).
return m.recipientRaceName === thread.raceName;
}
function displayBody(m: MailMessage): string {
if (m.translatedBody && !showOriginal.get(m.messageId)) {
return m.translatedBody;
}
return m.body;
}
function displaySubject(m: MailMessage): string {
if (m.translatedSubject && !showOriginal.get(m.messageId)) {
return m.translatedSubject ?? "";
}
return m.subject;
}
function toggleTranslation(messageId: string): void {
showOriginal.set(messageId, !(showOriginal.get(messageId) ?? false));
// Trigger reactivity on the map proxy.
showOriginal.size; // eslint-disable-line @typescript-eslint/no-unused-expressions
}
async function submitReply(event: SubmitEvent): Promise<void> {
event.preventDefault();
replyError = null;
const body = replyBody.trim();
if (body === "") {
replyError = i18n.t("game.mail.body_required");
return;
}
sending = true;
try {
await mailStore.composePersonal({
raceName: thread.raceName,
subject: "",
body,
});
replyBody = "";
} catch (err) {
replyError = err instanceof Error ? err.message : String(err);
} finally {
sending = false;
}
}
$effect(() => {
// Force the component to depend on the gameId rune so a game
// switch re-mounts the pane (the parent unmounts the old one
// implicitly via the entry-key change, but this keeps the
// dependency explicit for SSR-disabled hot reloads).
void gameId;
});
</script>
<div class="thread" data-testid="mail-thread-pane">
<h3 class="title">{thread.raceName}</h3>
<ol class="messages">
{#each thread.messages as m (m.messageId)}
<li class="message" class:outgoing={isOutgoing(m)}>
<div class="meta">
<span class="from">
{#if isOutgoing(m)}
{i18n.t("game.mail.outgoing_label")}
{:else}
{thread.raceName}
{/if}
</span>
<time>{m.createdAt.toISOString().slice(0, 19).replace("T", " ")}</time>
</div>
{#if displaySubject(m)}
<div class="subject">{displaySubject(m)}</div>
{/if}
<p class="body">{displayBody(m)}</p>
{#if m.translatedBody}
<button
type="button"
class="toggle"
onclick={() => toggleTranslation(m.messageId)}
>
{#if showOriginal.get(m.messageId)}
{i18n.t("game.mail.show_translation")}
{:else}
{i18n.t("game.mail.show_original")}
{/if}
</button>
{/if}
{#if !isOutgoing(m)}
<button
type="button"
class="delete"
onclick={() => mailStore.softDelete(m.messageId)}
data-testid="mail-delete"
>
{i18n.t("game.mail.delete_action")}
</button>
{/if}
</li>
{/each}
</ol>
<form class="reply" onsubmit={submitReply}>
<label for="mail-reply-body">{i18n.t("game.mail.reply_label")}</label>
<textarea
id="mail-reply-body"
bind:value={replyBody}
placeholder={i18n.t("game.mail.body_placeholder")}
rows="3"
data-testid="mail-reply-body"
></textarea>
{#if replyError}
<p class="error" data-testid="mail-reply-error">{replyError}</p>
{/if}
<button type="submit" disabled={sending} data-testid="mail-reply-send">
{i18n.t("game.mail.compose.send")}
</button>
</form>
</div>
<style>
.thread {
display: flex;
flex-direction: column;
gap: 0.75rem;
height: 100%;
}
.title {
margin: 0;
font-size: 1rem;
}
.messages {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
overflow-y: auto;
max-height: 50vh;
}
.message {
padding: 0.5rem 0.75rem;
border-radius: 6px;
background: #1c1c1c;
border: 1px solid #2a2a2a;
}
.message.outgoing {
background: #15252e;
}
.meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: #999;
margin-bottom: 0.25rem;
}
.subject {
font-weight: 700;
}
.body {
margin: 0.25rem 0 0;
white-space: pre-wrap;
}
.toggle,
.delete {
margin-top: 0.5rem;
margin-right: 0.5rem;
font: inherit;
padding: 0.2rem 0.5rem;
border: 1px solid #444;
background: transparent;
color: inherit;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.reply {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.reply textarea {
font: inherit;
padding: 0.5rem;
border: 1px solid #444;
background: #111;
color: inherit;
border-radius: 4px;
resize: vertical;
}
.reply button {
align-self: flex-end;
font: inherit;
padding: 0.35rem 0.75rem;
border: 1px solid #444;
background: #1a1a1a;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.reply button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #c62828;
font-size: 0.85rem;
margin: 0;
}
</style>
+25 -1
View File
@@ -15,10 +15,13 @@ polishes microcopy.
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { mailStore } from "$lib/mail-store.svelte";
type Props = { gameId: string };
let { gameId }: Props = $props();
const mailUnread = $derived(mailStore.unreadCount);
let open = $state(false);
let rootEl: HTMLDivElement | null = $state(null);
@@ -122,9 +125,15 @@ polishes microcopy.
type="button"
role="menuitem"
data-testid="view-menu-item-mail"
class="with-badge"
onclick={() => go(`/games/${gameId}/mail`)}
>
{i18n.t("game.view.mail")}
<span>{i18n.t("game.view.mail")}</span>
{#if mailUnread > 0}
<span class="badge" data-testid="view-menu-item-mail-badge">
{i18n.t("game.view.mail.badge", { count: String(mailUnread) })}
</span>
{/if}
</button>
<button
type="button"
@@ -200,6 +209,21 @@ polishes microcopy.
border: 0;
cursor: pointer;
}
.surface > button.with-badge {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.surface > button.with-badge .badge {
min-width: 1.5rem;
padding: 0 0.4rem;
text-align: center;
font-size: 0.75rem;
border-radius: 999px;
background: #2a4d7d;
color: #fff;
}
.surface > button:hover,
.surface > details > summary:hover {
background: #1c2238;
+40
View File
@@ -123,6 +123,46 @@ const en = {
"game.view.report": "turn report",
"game.view.battle": "battle log",
"game.view.mail": "diplomatic mail",
"game.view.mail.badge": "{count}",
"game.events.mail_new.message": "new mail from {from}",
"game.events.mail_new.action": "view",
"game.mail.loading": "loading mail…",
"game.mail.load_failed": "could not load mail",
"game.mail.empty": "no diplomatic messages yet",
"game.mail.back": "back",
"game.mail.compose_action": "compose",
"game.mail.select_thread": "pick a thread on the left to read it",
"game.mail.broadcast.title": "your broadcast",
"game.mail.admin.title": "admin notification",
"game.mail.system.generic.title": "system message",
"game.mail.system.game_paused.title": "game paused",
"game.mail.system.game_cancelled.title": "game cancelled",
"game.mail.system.membership_removed.title": "membership removed",
"game.mail.system.membership_blocked.title": "membership blocked",
"game.mail.subject_placeholder": "subject (optional)",
"game.mail.body_placeholder": "your message…",
"game.mail.recipient_label": "race",
"game.mail.recipient_required": "pick a recipient race",
"game.mail.body_required": "the message body cannot be empty",
"game.mail.body_too_long": "the body exceeds the {limit} byte limit",
"game.mail.subject_too_long": "the subject exceeds the {limit} byte limit",
"game.mail.compose.send": "send",
"game.mail.compose.cancel": "cancel",
"game.mail.compose.target_personal": "personal",
"game.mail.compose.target_broadcast": "broadcast",
"game.mail.compose.target_admin": "admin",
"game.mail.compose.recipients_active": "active members",
"game.mail.compose.recipients_active_and_removed": "active + removed",
"game.mail.compose.recipients_all_members": "all members",
"game.mail.compose.target_label": "kind",
"game.mail.compose.recipients_label": "audience",
"game.mail.compose.send_failed": "send failed",
"game.mail.show_original": "show original",
"game.mail.show_translation": "show translation",
"game.mail.translation_unavailable": "translation unavailable",
"game.mail.reply_label": "reply",
"game.mail.delete_action": "delete",
"game.mail.outgoing_label": "you",
"game.view.designer.ship_class": "ship-class designer",
"game.view.designer.science": "science designer",
"game.sidebar.tab.calculator": "calculator",
+40
View File
@@ -124,6 +124,46 @@ const ru: Record<keyof typeof en, string> = {
"game.view.report": "отчёт хода",
"game.view.battle": "журнал боёв",
"game.view.mail": "дипломатическая почта",
"game.view.mail.badge": "{count}",
"game.events.mail_new.message": "новое письмо от {from}",
"game.events.mail_new.action": "открыть",
"game.mail.loading": "загрузка почты…",
"game.mail.load_failed": "не удалось загрузить почту",
"game.mail.empty": "дипломатических сообщений пока нет",
"game.mail.back": "назад",
"game.mail.compose_action": "написать",
"game.mail.select_thread": "выбери ветку слева",
"game.mail.broadcast.title": "твоя рассылка",
"game.mail.admin.title": "административное уведомление",
"game.mail.system.generic.title": "системное сообщение",
"game.mail.system.game_paused.title": "игра поставлена на паузу",
"game.mail.system.game_cancelled.title": "игра отменена",
"game.mail.system.membership_removed.title": "членство удалено",
"game.mail.system.membership_blocked.title": "членство заблокировано",
"game.mail.subject_placeholder": "тема (необязательно)",
"game.mail.body_placeholder": "твоё сообщение…",
"game.mail.recipient_label": "раса",
"game.mail.recipient_required": "выбери расу-получателя",
"game.mail.body_required": "тело сообщения не может быть пустым",
"game.mail.body_too_long": "длина тела превышает лимит {limit} байт",
"game.mail.subject_too_long": "длина темы превышает лимит {limit} байт",
"game.mail.compose.send": "отправить",
"game.mail.compose.cancel": "отмена",
"game.mail.compose.target_personal": "личное",
"game.mail.compose.target_broadcast": "рассылка",
"game.mail.compose.target_admin": "админ.",
"game.mail.compose.recipients_active": "активным членам",
"game.mail.compose.recipients_active_and_removed": "активным + удалённым",
"game.mail.compose.recipients_all_members": "всем членам",
"game.mail.compose.target_label": "тип",
"game.mail.compose.recipients_label": "адресаты",
"game.mail.compose.send_failed": "отправка не удалась",
"game.mail.show_original": "показать оригинал",
"game.mail.show_translation": "показать перевод",
"game.mail.translation_unavailable": "перевод недоступен",
"game.mail.reply_label": "ответить",
"game.mail.delete_action": "удалить",
"game.mail.outgoing_label": "ты",
"game.view.designer.ship_class": "конструктор класса кораблей",
"game.view.designer.science": "редактор наук",
"game.sidebar.tab.calculator": "калькулятор",
+373
View File
@@ -0,0 +1,373 @@
// Phase 28 reactive store for the in-game diplomatic-mail view. Owns
// the inbox / sent listings, the per-race threading projection, the
// unread badge counter, and the imperative compose / mark-read /
// delete actions. The companion API wrappers live in
// `src/api/diplomail.ts`; this store coordinates them with the rest
// of the in-game shell.
import type { GalaxyClient } from "../api/galaxy-client";
import type { Cache } from "../platform/store/index";
import {
deleteMessage,
fetchInbox,
fetchMessage,
fetchSent,
markRead,
sendAdmin,
sendBroadcast,
sendPersonal,
type MailMessage,
type SendAdminArgs,
type SendBroadcastArgs,
type SendPersonalArgs,
} from "../api/diplomail";
/**
* MailThread groups personal messages exchanged with a single other
* race into one entry. The local player's outgoing messages live
* alongside incoming messages from the same race so the UI renders a
* chat-style transcript. `unreadCount` counts only incoming messages
* with `readAt === null`.
*/
export interface MailThread {
kind: "thread";
raceName: string;
messages: MailMessage[];
unreadCount: number;
latestAt: Date;
}
/**
* MailStandalone wraps a single message that does not participate in
* a race-thread: system mail, admin notifications, and the caller's
* own paid-tier broadcasts. The UI renders these as read-only items
* in the same list as the per-race threads.
*/
export interface MailStandalone {
kind: "standalone";
message: MailMessage;
latestAt: Date;
}
export type MailListEntry = MailThread | MailStandalone;
const CACHE_NAMESPACE = "diplomail";
/**
* MailStore is the reactive surface consumed by the active view, the
* header badge, and the push-event handler. One instance per signed-
* in session is enough — the rune fields are scoped to the current
* game and replaced on every `setGame` call so navigating between
* games stays clean.
*/
export class MailStore {
gameId = $state("");
status: "idle" | "loading" | "ready" | "error" = $state("idle");
error: string | null = $state(null);
inbox: MailMessage[] = $state([]);
sent: MailMessage[] = $state([]);
private client: GalaxyClient | null = null;
private cache: Cache | null = null;
/**
* entries surfaces the unified list-pane projection: per-race
* threads built from incoming + outgoing personal messages plus
* stand-alone items for system / admin / own-broadcast rows.
* Sorted newest-first by the latest message inside each entry.
*/
entries: MailListEntry[] = $derived.by(() => buildEntries(this.inbox, this.sent));
/**
* unreadCount drives the header view-menu badge. Counts only
* incoming personal / admin / system messages with `readAt === null`.
* `read_at` is not surfaced to the user in the UI but still
* drives this counter.
*/
unreadCount = $derived.by(() => this.inbox.reduce((acc, m) => (m.readAt === null ? acc + 1 : acc), 0));
/**
* init configures the dependencies and fires the initial fetch.
* Safe to call multiple times — calls after the first one are
* routed to `setGame`. `localUserId` is captured so the threading
* projection can tell outgoing messages from incoming when the
* inbox and sent lists are unified.
*/
async init(opts: {
client: GalaxyClient;
cache: Cache;
gameId: string;
}): Promise<void> {
this.client = opts.client;
this.cache = opts.cache;
await this.setGame(opts.gameId);
}
/**
* setGame switches the store to a different game id and refreshes
* its inbox / sent state. Idempotent on the same id — the network
* fetch fires only when the id actually changed or the previous
* load ended in `error`.
*/
async setGame(gameId: string): Promise<void> {
if (this.client === null) {
throw new Error("mail-store: setGame called before init");
}
if (this.gameId === gameId && this.status === "ready") {
return;
}
this.gameId = gameId;
this.status = "loading";
this.error = null;
this.inbox = [];
this.sent = [];
try {
const [inbox, sent] = await Promise.all([
fetchInbox(this.client, gameId),
fetchSent(this.client, gameId),
]);
this.inbox = inbox;
this.sent = sent;
this.status = "ready";
await this.rememberLastSeen();
} catch (err) {
this.status = "error";
this.error = errorMessage(err);
}
}
/** refresh re-fetches inbox + sent for the active game. */
async refresh(): Promise<void> {
if (this.gameId === "") {
return;
}
await this.setGame(this.gameId);
}
/**
* applyPushEvent reacts to a verified `diplomail.message.received`
* push frame by refetching the inbox for the active game. The
* payload carries only a preview, so the store hits the server for
* the canonical row.
*/
async applyPushEvent(payloadGameId: string): Promise<void> {
if (payloadGameId !== this.gameId || this.client === null) {
return;
}
try {
this.inbox = await fetchInbox(this.client, this.gameId);
await this.rememberLastSeen();
} catch (err) {
this.error = errorMessage(err);
}
}
/**
* markRead transitions an incoming message to `read`. The local
* inbox row is flipped optimistically; on failure the previous
* state is restored and the error surfaces via `error`.
*/
async markRead(messageId: string): Promise<void> {
if (this.client === null) {
return;
}
const before = this.inbox;
this.inbox = before.map((m) => {
if (m.messageId !== messageId) {
return m;
}
return { ...m, readAt: m.readAt ?? new Date() };
});
try {
await markRead(this.client, this.gameId, messageId);
} catch (err) {
this.inbox = before;
this.error = errorMessage(err);
}
}
/**
* softDelete removes a read incoming message from the inbox. The
* server enforces "read before delete"; on conflict the row is
* restored and the error surfaces.
*/
async softDelete(messageId: string): Promise<void> {
if (this.client === null) {
return;
}
const before = this.inbox;
this.inbox = before.filter((m) => m.messageId !== messageId);
try {
await deleteMessage(this.client, this.gameId, messageId);
} catch (err) {
this.inbox = before;
this.error = errorMessage(err);
}
}
/**
* composePersonal sends a single-recipient personal message,
* addressed by race name (resolved server-side). On success the
* resulting row is appended to the sent list so the matching
* thread surfaces it immediately. Throws on failure so callers
* can render inline form errors.
*/
async composePersonal(input: Omit<SendPersonalArgs, "gameId">): Promise<MailMessage> {
if (this.client === null) {
throw new Error("mail-store: composePersonal called before init");
}
const created = await sendPersonal(this.client, { ...input, gameId: this.gameId });
this.sent = [created, ...this.sent];
return created;
}
/**
* composeBroadcast posts a paid-tier player broadcast. The sent
* list is refreshed to surface the new entries.
*/
async composeBroadcast(input: Omit<SendBroadcastArgs, "gameId">): Promise<void> {
if (this.client === null) {
throw new Error("mail-store: composeBroadcast called before init");
}
await sendBroadcast(this.client, { ...input, gameId: this.gameId });
this.sent = await fetchSent(this.client, this.gameId);
}
/**
* composeAdmin posts an owner-only admin notification. Single
* sends refresh the sent list; broadcasts also refresh the sent
* list (the author does not appear as a recipient and is excluded
* from the resulting fan-out).
*/
async composeAdmin(input: Omit<SendAdminArgs, "gameId">): Promise<void> {
if (this.client === null) {
throw new Error("mail-store: composeAdmin called before init");
}
await sendAdmin(this.client, { ...input, gameId: this.gameId });
this.sent = await fetchSent(this.client, this.gameId);
}
/**
* loadMessage fetches a single message detail (used when the UI
* needs the freshest translation status for a specific row).
* The returned row is merged into the inbox copy if it lives
* there; sent rows are not refreshed here.
*/
async loadMessage(messageId: string): Promise<MailMessage | null> {
if (this.client === null) {
return null;
}
try {
const fresh = await fetchMessage(this.client, this.gameId, messageId);
this.inbox = this.inbox.map((m) => (m.messageId === messageId ? fresh : m));
return fresh;
} catch (err) {
this.error = errorMessage(err);
return null;
}
}
private async rememberLastSeen(): Promise<void> {
if (this.cache === null || this.gameId === "" || this.inbox.length === 0) {
return;
}
const last = this.inbox[0];
await this.cache.put(CACHE_NAMESPACE, `${this.gameId}/last-seen`, last.messageId);
}
}
function buildEntries(inbox: MailMessage[], sent: MailMessage[]): MailListEntry[] {
// Each personal message keyed by another race contributes to a
// race thread. Other shapes become stand-alone entries.
const threadsByRace = new Map<string, MailThread>();
const standalones: MailStandalone[] = [];
for (const m of inbox) {
if (isStandaloneIncoming(m)) {
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
continue;
}
const race = m.senderRaceName ?? "";
if (race === "") {
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
continue;
}
mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/true);
}
for (const m of sent) {
if (isStandaloneOutgoing(m)) {
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
continue;
}
const race = m.recipientRaceName ?? "";
if (race === "") {
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
continue;
}
mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/false);
}
// Sort each thread's messages oldest → newest for chat-style
// rendering; the entry list itself sorts newest-first by the
// most-recent message timestamp.
for (const thread of threadsByRace.values()) {
thread.messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
const last = thread.messages[thread.messages.length - 1];
thread.latestAt = last.createdAt;
}
const entries: MailListEntry[] = [
...Array.from(threadsByRace.values()),
...standalones,
];
entries.sort((a, b) => b.latestAt.getTime() - a.latestAt.getTime());
return entries;
}
function mergeIntoThread(
threads: Map<string, MailThread>,
race: string,
message: MailMessage,
isIncoming: boolean,
): void {
let thread = threads.get(race);
if (thread === undefined) {
thread = {
kind: "thread",
raceName: race,
messages: [],
unreadCount: 0,
latestAt: message.createdAt,
};
threads.set(race, thread);
}
thread.messages.push(message);
if (isIncoming && message.readAt === null) {
thread.unreadCount += 1;
}
if (message.createdAt.getTime() > thread.latestAt.getTime()) {
thread.latestAt = message.createdAt;
}
}
function isStandaloneIncoming(m: MailMessage): boolean {
// System / admin notifications never thread by race even when a
// snapshot is available — they are one-way operational mail.
return m.senderKind !== "player";
}
function isStandaloneOutgoing(m: MailMessage): boolean {
// Paid-tier broadcasts that the caller authored target many
// recipients; the UI renders them once as a stand-alone item.
return m.broadcastScope !== "single";
}
function errorMessage(err: unknown): string {
if (err instanceof Error) {
return err.message;
}
return String(err);
}
export const mailStore = new MailStore();
@@ -0,0 +1,23 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
export { AdminRequest, AdminRequestT } from './diplomail/admin-request.js';
export { AdminResponse, AdminResponseT } from './diplomail/admin-response.js';
export { BroadcastRequest, BroadcastRequestT } from './diplomail/broadcast-request.js';
export { BroadcastResponse, BroadcastResponseT } from './diplomail/broadcast-response.js';
export { DeleteRequest, DeleteRequestT } from './diplomail/delete-request.js';
export { DeleteResponse, DeleteResponseT } from './diplomail/delete-response.js';
export { InboxRequest, InboxRequestT } from './diplomail/inbox-request.js';
export { InboxResponse, InboxResponseT } from './diplomail/inbox-response.js';
export { MailBroadcastReceipt, MailBroadcastReceiptT } from './diplomail/mail-broadcast-receipt.js';
export { MailMessage, MailMessageT } from './diplomail/mail-message.js';
export { MailRecipientState, MailRecipientStateT } from './diplomail/mail-recipient-state.js';
export { MessageGetRequest, MessageGetRequestT } from './diplomail/message-get-request.js';
export { MessageGetResponse, MessageGetResponseT } from './diplomail/message-get-response.js';
export { ReadRequest, ReadRequestT } from './diplomail/read-request.js';
export { ReadResponse, ReadResponseT } from './diplomail/read-response.js';
export { SendRequest, SendRequestT } from './diplomail/send-request.js';
export { SendResponse, SendResponseT } from './diplomail/send-response.js';
export { SentRequest, SentRequestT } from './diplomail/sent-request.js';
export { SentResponse, SentResponseT } from './diplomail/sent-response.js';
@@ -0,0 +1,179 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class AdminRequest implements flatbuffers.IUnpackableObject<AdminRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):AdminRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsAdminRequest(bb:flatbuffers.ByteBuffer, obj?:AdminRequest):AdminRequest {
return (obj || new AdminRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsAdminRequest(bb:flatbuffers.ByteBuffer, obj?:AdminRequest):AdminRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new AdminRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
target():string|null
target(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
target(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipientUserId():string|null
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipientRaceName():string|null
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipients():string|null
recipients(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipients(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startAdminRequest(builder:flatbuffers.Builder) {
builder.startObject(7);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addTarget(builder:flatbuffers.Builder, targetOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, targetOffset, 0);
}
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, recipientUserIdOffset, 0);
}
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, recipientRaceNameOffset, 0);
}
static addRecipients(builder:flatbuffers.Builder, recipientsOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, recipientsOffset, 0);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, subjectOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(6, bodyOffset, 0);
}
static endAdminRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createAdminRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, targetOffset:flatbuffers.Offset, recipientUserIdOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, recipientsOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
AdminRequest.startAdminRequest(builder);
AdminRequest.addGameId(builder, gameIdOffset);
AdminRequest.addTarget(builder, targetOffset);
AdminRequest.addRecipientUserId(builder, recipientUserIdOffset);
AdminRequest.addRecipientRaceName(builder, recipientRaceNameOffset);
AdminRequest.addRecipients(builder, recipientsOffset);
AdminRequest.addSubject(builder, subjectOffset);
AdminRequest.addBody(builder, bodyOffset);
return AdminRequest.endAdminRequest(builder);
}
unpack(): AdminRequestT {
return new AdminRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.target(),
this.recipientUserId(),
this.recipientRaceName(),
this.recipients(),
this.subject(),
this.body()
);
}
unpackTo(_o: AdminRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.target = this.target();
_o.recipientUserId = this.recipientUserId();
_o.recipientRaceName = this.recipientRaceName();
_o.recipients = this.recipients();
_o.subject = this.subject();
_o.body = this.body();
}
}
export class AdminRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public target: string|Uint8Array|null = null,
public recipientUserId: string|Uint8Array|null = null,
public recipientRaceName: string|Uint8Array|null = null,
public recipients: string|Uint8Array|null = null,
public subject: string|Uint8Array|null = null,
public body: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const target = (this.target !== null ? builder.createString(this.target!) : 0);
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
const recipients = (this.recipients !== null ? builder.createString(this.recipients!) : 0);
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
const body = (this.body !== null ? builder.createString(this.body!) : 0);
return AdminRequest.createAdminRequest(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
target,
recipientUserId,
recipientRaceName,
recipients,
subject,
body
);
}
}
@@ -0,0 +1,88 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailBroadcastReceipt, MailBroadcastReceiptT } from '../diplomail/mail-broadcast-receipt.js';
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
export class AdminResponse implements flatbuffers.IUnpackableObject<AdminResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):AdminResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsAdminResponse(bb:flatbuffers.ByteBuffer, obj?:AdminResponse):AdminResponse {
return (obj || new AdminResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsAdminResponse(bb:flatbuffers.ByteBuffer, obj?:AdminResponse):AdminResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new AdminResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
message(obj?:MailMessage):MailMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
receipt(obj?:MailBroadcastReceipt):MailBroadcastReceipt|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new MailBroadcastReceipt()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startAdminResponse(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageOffset, 0);
}
static addReceipt(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, receiptOffset, 0);
}
static endAdminResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
unpack(): AdminResponseT {
return new AdminResponseT(
(this.message() !== null ? this.message()!.unpack() : null),
(this.receipt() !== null ? this.receipt()!.unpack() : null)
);
}
unpackTo(_o: AdminResponseT): void {
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
_o.receipt = (this.receipt() !== null ? this.receipt()!.unpack() : null);
}
}
export class AdminResponseT implements flatbuffers.IGeneratedObject {
constructor(
public message: MailMessageT|null = null,
public receipt: MailBroadcastReceiptT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const message = (this.message !== null ? this.message!.pack(builder) : 0);
const receipt = (this.receipt !== null ? this.receipt!.pack(builder) : 0);
AdminResponse.startAdminResponse(builder);
AdminResponse.addMessage(builder, message);
AdminResponse.addReceipt(builder, receipt);
return AdminResponse.endAdminResponse(builder);
}
}
@@ -0,0 +1,111 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class BroadcastRequest implements flatbuffers.IUnpackableObject<BroadcastRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):BroadcastRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsBroadcastRequest(bb:flatbuffers.ByteBuffer, obj?:BroadcastRequest):BroadcastRequest {
return (obj || new BroadcastRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsBroadcastRequest(bb:flatbuffers.ByteBuffer, obj?:BroadcastRequest):BroadcastRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new BroadcastRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startBroadcastRequest(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, subjectOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, bodyOffset, 0);
}
static endBroadcastRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createBroadcastRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
BroadcastRequest.startBroadcastRequest(builder);
BroadcastRequest.addGameId(builder, gameIdOffset);
BroadcastRequest.addSubject(builder, subjectOffset);
BroadcastRequest.addBody(builder, bodyOffset);
return BroadcastRequest.endBroadcastRequest(builder);
}
unpack(): BroadcastRequestT {
return new BroadcastRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.subject(),
this.body()
);
}
unpackTo(_o: BroadcastRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.subject = this.subject();
_o.body = this.body();
}
}
export class BroadcastRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public subject: string|Uint8Array|null = null,
public body: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
const body = (this.body !== null ? builder.createString(this.body!) : 0);
return BroadcastRequest.createBroadcastRequest(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
subject,
body
);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailBroadcastReceipt, MailBroadcastReceiptT } from '../diplomail/mail-broadcast-receipt.js';
export class BroadcastResponse implements flatbuffers.IUnpackableObject<BroadcastResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):BroadcastResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsBroadcastResponse(bb:flatbuffers.ByteBuffer, obj?:BroadcastResponse):BroadcastResponse {
return (obj || new BroadcastResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsBroadcastResponse(bb:flatbuffers.ByteBuffer, obj?:BroadcastResponse):BroadcastResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new BroadcastResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
receipt(obj?:MailBroadcastReceipt):MailBroadcastReceipt|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailBroadcastReceipt()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startBroadcastResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addReceipt(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, receiptOffset, 0);
}
static endBroadcastResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createBroadcastResponse(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset):flatbuffers.Offset {
BroadcastResponse.startBroadcastResponse(builder);
BroadcastResponse.addReceipt(builder, receiptOffset);
return BroadcastResponse.endBroadcastResponse(builder);
}
unpack(): BroadcastResponseT {
return new BroadcastResponseT(
(this.receipt() !== null ? this.receipt()!.unpack() : null)
);
}
unpackTo(_o: BroadcastResponseT): void {
_o.receipt = (this.receipt() !== null ? this.receipt()!.unpack() : null);
}
}
export class BroadcastResponseT implements flatbuffers.IGeneratedObject {
constructor(
public receipt: MailBroadcastReceiptT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const receipt = (this.receipt !== null ? this.receipt!.pack(builder) : 0);
return BroadcastResponse.createBroadcastResponse(builder,
receipt
);
}
}
@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class DeleteRequest implements flatbuffers.IUnpackableObject<DeleteRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):DeleteRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsDeleteRequest(bb:flatbuffers.ByteBuffer, obj?:DeleteRequest):DeleteRequest {
return (obj || new DeleteRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsDeleteRequest(bb:flatbuffers.ByteBuffer, obj?:DeleteRequest):DeleteRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new DeleteRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
messageId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startDeleteRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(1, messageIdOffset, 0);
}
static endDeleteRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
builder.requiredField(offset, 6) // message_id
return offset;
}
unpack(): DeleteRequestT {
return new DeleteRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
(this.messageId() !== null ? this.messageId()!.unpack() : null)
);
}
unpackTo(_o: DeleteRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
}
}
export class DeleteRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public messageId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
DeleteRequest.startDeleteRequest(builder);
DeleteRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
DeleteRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
return DeleteRequest.endDeleteRequest(builder);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailRecipientState, MailRecipientStateT } from '../diplomail/mail-recipient-state.js';
export class DeleteResponse implements flatbuffers.IUnpackableObject<DeleteResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):DeleteResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsDeleteResponse(bb:flatbuffers.ByteBuffer, obj?:DeleteResponse):DeleteResponse {
return (obj || new DeleteResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsDeleteResponse(bb:flatbuffers.ByteBuffer, obj?:DeleteResponse):DeleteResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new DeleteResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
state(obj?:MailRecipientState):MailRecipientState|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailRecipientState()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startDeleteResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addState(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, stateOffset, 0);
}
static endDeleteResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createDeleteResponse(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset):flatbuffers.Offset {
DeleteResponse.startDeleteResponse(builder);
DeleteResponse.addState(builder, stateOffset);
return DeleteResponse.endDeleteResponse(builder);
}
unpack(): DeleteResponseT {
return new DeleteResponseT(
(this.state() !== null ? this.state()!.unpack() : null)
);
}
unpackTo(_o: DeleteResponseT): void {
_o.state = (this.state() !== null ? this.state()!.unpack() : null);
}
}
export class DeleteResponseT implements flatbuffers.IGeneratedObject {
constructor(
public state: MailRecipientStateT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const state = (this.state !== null ? this.state!.pack(builder) : 0);
return DeleteResponse.createDeleteResponse(builder,
state
);
}
}
@@ -0,0 +1,76 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class InboxRequest implements flatbuffers.IUnpackableObject<InboxRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):InboxRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsInboxRequest(bb:flatbuffers.ByteBuffer, obj?:InboxRequest):InboxRequest {
return (obj || new InboxRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsInboxRequest(bb:flatbuffers.ByteBuffer, obj?:InboxRequest):InboxRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new InboxRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startInboxRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static endInboxRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createInboxRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
InboxRequest.startInboxRequest(builder);
InboxRequest.addGameId(builder, gameIdOffset);
return InboxRequest.endInboxRequest(builder);
}
unpack(): InboxRequestT {
return new InboxRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null)
);
}
unpackTo(_o: InboxRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
}
}
export class InboxRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return InboxRequest.createInboxRequest(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0)
);
}
}
@@ -0,0 +1,94 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
export class InboxResponse implements flatbuffers.IUnpackableObject<InboxResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):InboxResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsInboxResponse(bb:flatbuffers.ByteBuffer, obj?:InboxResponse):InboxResponse {
return (obj || new InboxResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsInboxResponse(bb:flatbuffers.ByteBuffer, obj?:InboxResponse):InboxResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new InboxResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
items(index: number, obj?:MailMessage):MailMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
itemsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startInboxResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addItems(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, itemsOffset, 0);
}
static createItemsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startItemsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endInboxResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createInboxResponse(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset):flatbuffers.Offset {
InboxResponse.startInboxResponse(builder);
InboxResponse.addItems(builder, itemsOffset);
return InboxResponse.endInboxResponse(builder);
}
unpack(): InboxResponseT {
return new InboxResponseT(
this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength())
);
}
unpackTo(_o: InboxResponseT): void {
_o.items = this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength());
}
}
export class InboxResponseT implements flatbuffers.IGeneratedObject {
constructor(
public items: (MailMessageT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const items = InboxResponse.createItemsVector(builder, builder.createObjectOffsetList(this.items));
return InboxResponse.createInboxResponse(builder,
items
);
}
}
@@ -0,0 +1,242 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class MailBroadcastReceipt implements flatbuffers.IUnpackableObject<MailBroadcastReceiptT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MailBroadcastReceipt {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMailBroadcastReceipt(bb:flatbuffers.ByteBuffer, obj?:MailBroadcastReceipt):MailBroadcastReceipt {
return (obj || new MailBroadcastReceipt()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMailBroadcastReceipt(bb:flatbuffers.ByteBuffer, obj?:MailBroadcastReceipt):MailBroadcastReceipt {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MailBroadcastReceipt()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
messageId():string|null
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
messageId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
gameName():string|null
gameName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
kind():string|null
kind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
kind(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderKind():string|null
senderKind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderKind(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
bodyLang():string|null
bodyLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
bodyLang(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 18);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
broadcastScope():string|null
broadcastScope(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
broadcastScope(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 20);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
createdAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 22);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
recipientCount():number {
const offset = this.bb!.__offset(this.bb_pos, 24);
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
}
static startMailBroadcastReceipt(builder:flatbuffers.Builder) {
builder.startObject(11);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageIdOffset, 0);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, gameIdOffset, 0);
}
static addGameName(builder:flatbuffers.Builder, gameNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, gameNameOffset, 0);
}
static addKind(builder:flatbuffers.Builder, kindOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, kindOffset, 0);
}
static addSenderKind(builder:flatbuffers.Builder, senderKindOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, senderKindOffset, 0);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, subjectOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(6, bodyOffset, 0);
}
static addBodyLang(builder:flatbuffers.Builder, bodyLangOffset:flatbuffers.Offset) {
builder.addFieldOffset(7, bodyLangOffset, 0);
}
static addBroadcastScope(builder:flatbuffers.Builder, broadcastScopeOffset:flatbuffers.Offset) {
builder.addFieldOffset(8, broadcastScopeOffset, 0);
}
static addCreatedAtMs(builder:flatbuffers.Builder, createdAtMs:bigint) {
builder.addFieldInt64(9, createdAtMs, BigInt('0'));
}
static addRecipientCount(builder:flatbuffers.Builder, recipientCount:number) {
builder.addFieldInt32(10, recipientCount, 0);
}
static endMailBroadcastReceipt(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMailBroadcastReceipt(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, gameNameOffset:flatbuffers.Offset, kindOffset:flatbuffers.Offset, senderKindOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset, bodyLangOffset:flatbuffers.Offset, broadcastScopeOffset:flatbuffers.Offset, createdAtMs:bigint, recipientCount:number):flatbuffers.Offset {
MailBroadcastReceipt.startMailBroadcastReceipt(builder);
MailBroadcastReceipt.addMessageId(builder, messageIdOffset);
MailBroadcastReceipt.addGameId(builder, gameIdOffset);
MailBroadcastReceipt.addGameName(builder, gameNameOffset);
MailBroadcastReceipt.addKind(builder, kindOffset);
MailBroadcastReceipt.addSenderKind(builder, senderKindOffset);
MailBroadcastReceipt.addSubject(builder, subjectOffset);
MailBroadcastReceipt.addBody(builder, bodyOffset);
MailBroadcastReceipt.addBodyLang(builder, bodyLangOffset);
MailBroadcastReceipt.addBroadcastScope(builder, broadcastScopeOffset);
MailBroadcastReceipt.addCreatedAtMs(builder, createdAtMs);
MailBroadcastReceipt.addRecipientCount(builder, recipientCount);
return MailBroadcastReceipt.endMailBroadcastReceipt(builder);
}
unpack(): MailBroadcastReceiptT {
return new MailBroadcastReceiptT(
this.messageId(),
this.gameId(),
this.gameName(),
this.kind(),
this.senderKind(),
this.subject(),
this.body(),
this.bodyLang(),
this.broadcastScope(),
this.createdAtMs(),
this.recipientCount()
);
}
unpackTo(_o: MailBroadcastReceiptT): void {
_o.messageId = this.messageId();
_o.gameId = this.gameId();
_o.gameName = this.gameName();
_o.kind = this.kind();
_o.senderKind = this.senderKind();
_o.subject = this.subject();
_o.body = this.body();
_o.bodyLang = this.bodyLang();
_o.broadcastScope = this.broadcastScope();
_o.createdAtMs = this.createdAtMs();
_o.recipientCount = this.recipientCount();
}
}
export class MailBroadcastReceiptT implements flatbuffers.IGeneratedObject {
constructor(
public messageId: string|Uint8Array|null = null,
public gameId: string|Uint8Array|null = null,
public gameName: string|Uint8Array|null = null,
public kind: string|Uint8Array|null = null,
public senderKind: string|Uint8Array|null = null,
public subject: string|Uint8Array|null = null,
public body: string|Uint8Array|null = null,
public bodyLang: string|Uint8Array|null = null,
public broadcastScope: string|Uint8Array|null = null,
public createdAtMs: bigint = BigInt('0'),
public recipientCount: number = 0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
const gameId = (this.gameId !== null ? builder.createString(this.gameId!) : 0);
const gameName = (this.gameName !== null ? builder.createString(this.gameName!) : 0);
const kind = (this.kind !== null ? builder.createString(this.kind!) : 0);
const senderKind = (this.senderKind !== null ? builder.createString(this.senderKind!) : 0);
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
const body = (this.body !== null ? builder.createString(this.body!) : 0);
const bodyLang = (this.bodyLang !== null ? builder.createString(this.bodyLang!) : 0);
const broadcastScope = (this.broadcastScope !== null ? builder.createString(this.broadcastScope!) : 0);
return MailBroadcastReceipt.createMailBroadcastReceipt(builder,
messageId,
gameId,
gameName,
kind,
senderKind,
subject,
body,
bodyLang,
broadcastScope,
this.createdAtMs,
this.recipientCount
);
}
}
@@ -0,0 +1,426 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class MailMessage implements flatbuffers.IUnpackableObject<MailMessageT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MailMessage {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMailMessage(bb:flatbuffers.ByteBuffer, obj?:MailMessage):MailMessage {
return (obj || new MailMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMailMessage(bb:flatbuffers.ByteBuffer, obj?:MailMessage):MailMessage {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MailMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
messageId():string|null
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
messageId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
gameId():string|null
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
gameName():string|null
gameName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
gameName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
kind():string|null
kind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
kind(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderKind():string|null
senderKind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderKind(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderUserId():string|null
senderUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderUserId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderUsername():string|null
senderUsername(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderUsername(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 16);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
senderRaceName():string|null
senderRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
senderRaceName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 18);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 20);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 22);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
bodyLang():string|null
bodyLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
bodyLang(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 24);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
broadcastScope():string|null
broadcastScope(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
broadcastScope(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 26);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
createdAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 28);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
recipientUserId():string|null
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 30);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipientUserName():string|null
recipientUserName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientUserName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 32);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipientRaceName():string|null
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 34);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
readAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 36);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
deletedAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 38);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
translatedSubject():string|null
translatedSubject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
translatedSubject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 40);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
translatedBody():string|null
translatedBody(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
translatedBody(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 42);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
translationLang():string|null
translationLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
translationLang(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 44);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
translator():string|null
translator(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
translator(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 46);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startMailMessage(builder:flatbuffers.Builder) {
builder.startObject(22);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageIdOffset, 0);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, gameIdOffset, 0);
}
static addGameName(builder:flatbuffers.Builder, gameNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, gameNameOffset, 0);
}
static addKind(builder:flatbuffers.Builder, kindOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, kindOffset, 0);
}
static addSenderKind(builder:flatbuffers.Builder, senderKindOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, senderKindOffset, 0);
}
static addSenderUserId(builder:flatbuffers.Builder, senderUserIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, senderUserIdOffset, 0);
}
static addSenderUsername(builder:flatbuffers.Builder, senderUsernameOffset:flatbuffers.Offset) {
builder.addFieldOffset(6, senderUsernameOffset, 0);
}
static addSenderRaceName(builder:flatbuffers.Builder, senderRaceNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(7, senderRaceNameOffset, 0);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(8, subjectOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(9, bodyOffset, 0);
}
static addBodyLang(builder:flatbuffers.Builder, bodyLangOffset:flatbuffers.Offset) {
builder.addFieldOffset(10, bodyLangOffset, 0);
}
static addBroadcastScope(builder:flatbuffers.Builder, broadcastScopeOffset:flatbuffers.Offset) {
builder.addFieldOffset(11, broadcastScopeOffset, 0);
}
static addCreatedAtMs(builder:flatbuffers.Builder, createdAtMs:bigint) {
builder.addFieldInt64(12, createdAtMs, BigInt('0'));
}
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(13, recipientUserIdOffset, 0);
}
static addRecipientUserName(builder:flatbuffers.Builder, recipientUserNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(14, recipientUserNameOffset, 0);
}
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(15, recipientRaceNameOffset, 0);
}
static addReadAtMs(builder:flatbuffers.Builder, readAtMs:bigint) {
builder.addFieldInt64(16, readAtMs, BigInt('0'));
}
static addDeletedAtMs(builder:flatbuffers.Builder, deletedAtMs:bigint) {
builder.addFieldInt64(17, deletedAtMs, BigInt('0'));
}
static addTranslatedSubject(builder:flatbuffers.Builder, translatedSubjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(18, translatedSubjectOffset, 0);
}
static addTranslatedBody(builder:flatbuffers.Builder, translatedBodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(19, translatedBodyOffset, 0);
}
static addTranslationLang(builder:flatbuffers.Builder, translationLangOffset:flatbuffers.Offset) {
builder.addFieldOffset(20, translationLangOffset, 0);
}
static addTranslator(builder:flatbuffers.Builder, translatorOffset:flatbuffers.Offset) {
builder.addFieldOffset(21, translatorOffset, 0);
}
static endMailMessage(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMailMessage(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, gameNameOffset:flatbuffers.Offset, kindOffset:flatbuffers.Offset, senderKindOffset:flatbuffers.Offset, senderUserIdOffset:flatbuffers.Offset, senderUsernameOffset:flatbuffers.Offset, senderRaceNameOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset, bodyLangOffset:flatbuffers.Offset, broadcastScopeOffset:flatbuffers.Offset, createdAtMs:bigint, recipientUserIdOffset:flatbuffers.Offset, recipientUserNameOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, readAtMs:bigint, deletedAtMs:bigint, translatedSubjectOffset:flatbuffers.Offset, translatedBodyOffset:flatbuffers.Offset, translationLangOffset:flatbuffers.Offset, translatorOffset:flatbuffers.Offset):flatbuffers.Offset {
MailMessage.startMailMessage(builder);
MailMessage.addMessageId(builder, messageIdOffset);
MailMessage.addGameId(builder, gameIdOffset);
MailMessage.addGameName(builder, gameNameOffset);
MailMessage.addKind(builder, kindOffset);
MailMessage.addSenderKind(builder, senderKindOffset);
MailMessage.addSenderUserId(builder, senderUserIdOffset);
MailMessage.addSenderUsername(builder, senderUsernameOffset);
MailMessage.addSenderRaceName(builder, senderRaceNameOffset);
MailMessage.addSubject(builder, subjectOffset);
MailMessage.addBody(builder, bodyOffset);
MailMessage.addBodyLang(builder, bodyLangOffset);
MailMessage.addBroadcastScope(builder, broadcastScopeOffset);
MailMessage.addCreatedAtMs(builder, createdAtMs);
MailMessage.addRecipientUserId(builder, recipientUserIdOffset);
MailMessage.addRecipientUserName(builder, recipientUserNameOffset);
MailMessage.addRecipientRaceName(builder, recipientRaceNameOffset);
MailMessage.addReadAtMs(builder, readAtMs);
MailMessage.addDeletedAtMs(builder, deletedAtMs);
MailMessage.addTranslatedSubject(builder, translatedSubjectOffset);
MailMessage.addTranslatedBody(builder, translatedBodyOffset);
MailMessage.addTranslationLang(builder, translationLangOffset);
MailMessage.addTranslator(builder, translatorOffset);
return MailMessage.endMailMessage(builder);
}
unpack(): MailMessageT {
return new MailMessageT(
this.messageId(),
this.gameId(),
this.gameName(),
this.kind(),
this.senderKind(),
this.senderUserId(),
this.senderUsername(),
this.senderRaceName(),
this.subject(),
this.body(),
this.bodyLang(),
this.broadcastScope(),
this.createdAtMs(),
this.recipientUserId(),
this.recipientUserName(),
this.recipientRaceName(),
this.readAtMs(),
this.deletedAtMs(),
this.translatedSubject(),
this.translatedBody(),
this.translationLang(),
this.translator()
);
}
unpackTo(_o: MailMessageT): void {
_o.messageId = this.messageId();
_o.gameId = this.gameId();
_o.gameName = this.gameName();
_o.kind = this.kind();
_o.senderKind = this.senderKind();
_o.senderUserId = this.senderUserId();
_o.senderUsername = this.senderUsername();
_o.senderRaceName = this.senderRaceName();
_o.subject = this.subject();
_o.body = this.body();
_o.bodyLang = this.bodyLang();
_o.broadcastScope = this.broadcastScope();
_o.createdAtMs = this.createdAtMs();
_o.recipientUserId = this.recipientUserId();
_o.recipientUserName = this.recipientUserName();
_o.recipientRaceName = this.recipientRaceName();
_o.readAtMs = this.readAtMs();
_o.deletedAtMs = this.deletedAtMs();
_o.translatedSubject = this.translatedSubject();
_o.translatedBody = this.translatedBody();
_o.translationLang = this.translationLang();
_o.translator = this.translator();
}
}
export class MailMessageT implements flatbuffers.IGeneratedObject {
constructor(
public messageId: string|Uint8Array|null = null,
public gameId: string|Uint8Array|null = null,
public gameName: string|Uint8Array|null = null,
public kind: string|Uint8Array|null = null,
public senderKind: string|Uint8Array|null = null,
public senderUserId: string|Uint8Array|null = null,
public senderUsername: string|Uint8Array|null = null,
public senderRaceName: string|Uint8Array|null = null,
public subject: string|Uint8Array|null = null,
public body: string|Uint8Array|null = null,
public bodyLang: string|Uint8Array|null = null,
public broadcastScope: string|Uint8Array|null = null,
public createdAtMs: bigint = BigInt('0'),
public recipientUserId: string|Uint8Array|null = null,
public recipientUserName: string|Uint8Array|null = null,
public recipientRaceName: string|Uint8Array|null = null,
public readAtMs: bigint = BigInt('0'),
public deletedAtMs: bigint = BigInt('0'),
public translatedSubject: string|Uint8Array|null = null,
public translatedBody: string|Uint8Array|null = null,
public translationLang: string|Uint8Array|null = null,
public translator: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
const gameId = (this.gameId !== null ? builder.createString(this.gameId!) : 0);
const gameName = (this.gameName !== null ? builder.createString(this.gameName!) : 0);
const kind = (this.kind !== null ? builder.createString(this.kind!) : 0);
const senderKind = (this.senderKind !== null ? builder.createString(this.senderKind!) : 0);
const senderUserId = (this.senderUserId !== null ? builder.createString(this.senderUserId!) : 0);
const senderUsername = (this.senderUsername !== null ? builder.createString(this.senderUsername!) : 0);
const senderRaceName = (this.senderRaceName !== null ? builder.createString(this.senderRaceName!) : 0);
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
const body = (this.body !== null ? builder.createString(this.body!) : 0);
const bodyLang = (this.bodyLang !== null ? builder.createString(this.bodyLang!) : 0);
const broadcastScope = (this.broadcastScope !== null ? builder.createString(this.broadcastScope!) : 0);
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
const recipientUserName = (this.recipientUserName !== null ? builder.createString(this.recipientUserName!) : 0);
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
const translatedSubject = (this.translatedSubject !== null ? builder.createString(this.translatedSubject!) : 0);
const translatedBody = (this.translatedBody !== null ? builder.createString(this.translatedBody!) : 0);
const translationLang = (this.translationLang !== null ? builder.createString(this.translationLang!) : 0);
const translator = (this.translator !== null ? builder.createString(this.translator!) : 0);
return MailMessage.createMailMessage(builder,
messageId,
gameId,
gameName,
kind,
senderKind,
senderUserId,
senderUsername,
senderRaceName,
subject,
body,
bodyLang,
broadcastScope,
this.createdAtMs,
recipientUserId,
recipientUserName,
recipientRaceName,
this.readAtMs,
this.deletedAtMs,
translatedSubject,
translatedBody,
translationLang,
translator
);
}
}
@@ -0,0 +1,106 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
export class MailRecipientState implements flatbuffers.IUnpackableObject<MailRecipientStateT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MailRecipientState {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMailRecipientState(bb:flatbuffers.ByteBuffer, obj?:MailRecipientState):MailRecipientState {
return (obj || new MailRecipientState()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMailRecipientState(bb:flatbuffers.ByteBuffer, obj?:MailRecipientState):MailRecipientState {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MailRecipientState()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
messageId():string|null
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
messageId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
readAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
deletedAtMs():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
static startMailRecipientState(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageIdOffset, 0);
}
static addReadAtMs(builder:flatbuffers.Builder, readAtMs:bigint) {
builder.addFieldInt64(1, readAtMs, BigInt('0'));
}
static addDeletedAtMs(builder:flatbuffers.Builder, deletedAtMs:bigint) {
builder.addFieldInt64(2, deletedAtMs, BigInt('0'));
}
static endMailRecipientState(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMailRecipientState(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, readAtMs:bigint, deletedAtMs:bigint):flatbuffers.Offset {
MailRecipientState.startMailRecipientState(builder);
MailRecipientState.addMessageId(builder, messageIdOffset);
MailRecipientState.addReadAtMs(builder, readAtMs);
MailRecipientState.addDeletedAtMs(builder, deletedAtMs);
return MailRecipientState.endMailRecipientState(builder);
}
unpack(): MailRecipientStateT {
return new MailRecipientStateT(
this.messageId(),
this.readAtMs(),
this.deletedAtMs()
);
}
unpackTo(_o: MailRecipientStateT): void {
_o.messageId = this.messageId();
_o.readAtMs = this.readAtMs();
_o.deletedAtMs = this.deletedAtMs();
}
}
export class MailRecipientStateT implements flatbuffers.IGeneratedObject {
constructor(
public messageId: string|Uint8Array|null = null,
public readAtMs: bigint = BigInt('0'),
public deletedAtMs: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
return MailRecipientState.createMailRecipientState(builder,
messageId,
this.readAtMs,
this.deletedAtMs
);
}
}
@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class MessageGetRequest implements flatbuffers.IUnpackableObject<MessageGetRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MessageGetRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMessageGetRequest(bb:flatbuffers.ByteBuffer, obj?:MessageGetRequest):MessageGetRequest {
return (obj || new MessageGetRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMessageGetRequest(bb:flatbuffers.ByteBuffer, obj?:MessageGetRequest):MessageGetRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MessageGetRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
messageId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startMessageGetRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(1, messageIdOffset, 0);
}
static endMessageGetRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
builder.requiredField(offset, 6) // message_id
return offset;
}
unpack(): MessageGetRequestT {
return new MessageGetRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
(this.messageId() !== null ? this.messageId()!.unpack() : null)
);
}
unpackTo(_o: MessageGetRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
}
}
export class MessageGetRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public messageId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
MessageGetRequest.startMessageGetRequest(builder);
MessageGetRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
MessageGetRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
return MessageGetRequest.endMessageGetRequest(builder);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
export class MessageGetResponse implements flatbuffers.IUnpackableObject<MessageGetResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):MessageGetResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsMessageGetResponse(bb:flatbuffers.ByteBuffer, obj?:MessageGetResponse):MessageGetResponse {
return (obj || new MessageGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsMessageGetResponse(bb:flatbuffers.ByteBuffer, obj?:MessageGetResponse):MessageGetResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new MessageGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
message(obj?:MailMessage):MailMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startMessageGetResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageOffset, 0);
}
static endMessageGetResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createMessageGetResponse(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset):flatbuffers.Offset {
MessageGetResponse.startMessageGetResponse(builder);
MessageGetResponse.addMessage(builder, messageOffset);
return MessageGetResponse.endMessageGetResponse(builder);
}
unpack(): MessageGetResponseT {
return new MessageGetResponseT(
(this.message() !== null ? this.message()!.unpack() : null)
);
}
unpackTo(_o: MessageGetResponseT): void {
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
}
}
export class MessageGetResponseT implements flatbuffers.IGeneratedObject {
constructor(
public message: MailMessageT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const message = (this.message !== null ? this.message!.pack(builder) : 0);
return MessageGetResponse.createMessageGetResponse(builder,
message
);
}
}
@@ -0,0 +1,86 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class ReadRequest implements flatbuffers.IUnpackableObject<ReadRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRequest):ReadRequest {
return (obj || new ReadRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRequest):ReadRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
messageId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startReadRequest(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(1, messageIdOffset, 0);
}
static endReadRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
builder.requiredField(offset, 6) // message_id
return offset;
}
unpack(): ReadRequestT {
return new ReadRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
(this.messageId() !== null ? this.messageId()!.unpack() : null)
);
}
unpackTo(_o: ReadRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
}
}
export class ReadRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public messageId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
ReadRequest.startReadRequest(builder);
ReadRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
ReadRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
return ReadRequest.endReadRequest(builder);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailRecipientState, MailRecipientStateT } from '../diplomail/mail-recipient-state.js';
export class ReadResponse implements flatbuffers.IUnpackableObject<ReadResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ReadResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsReadResponse(bb:flatbuffers.ByteBuffer, obj?:ReadResponse):ReadResponse {
return (obj || new ReadResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsReadResponse(bb:flatbuffers.ByteBuffer, obj?:ReadResponse):ReadResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ReadResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
state(obj?:MailRecipientState):MailRecipientState|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailRecipientState()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startReadResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addState(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, stateOffset, 0);
}
static endReadResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createReadResponse(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset):flatbuffers.Offset {
ReadResponse.startReadResponse(builder);
ReadResponse.addState(builder, stateOffset);
return ReadResponse.endReadResponse(builder);
}
unpack(): ReadResponseT {
return new ReadResponseT(
(this.state() !== null ? this.state()!.unpack() : null)
);
}
unpackTo(_o: ReadResponseT): void {
_o.state = (this.state() !== null ? this.state()!.unpack() : null);
}
}
export class ReadResponseT implements flatbuffers.IGeneratedObject {
constructor(
public state: MailRecipientStateT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const state = (this.state !== null ? this.state!.pack(builder) : 0);
return ReadResponse.createReadResponse(builder,
state
);
}
}
@@ -0,0 +1,145 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class SendRequest implements flatbuffers.IUnpackableObject<SendRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SendRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest {
return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
recipientUserId():string|null
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
recipientRaceName():string|null
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
subject():string|null
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
subject(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
body():string|null
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
body(optionalEncoding?:any):string|Uint8Array|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
}
static startSendRequest(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, recipientUserIdOffset, 0);
}
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, recipientRaceNameOffset, 0);
}
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, subjectOffset, 0);
}
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, bodyOffset, 0);
}
static endSendRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createSendRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, recipientUserIdOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
SendRequest.startSendRequest(builder);
SendRequest.addGameId(builder, gameIdOffset);
SendRequest.addRecipientUserId(builder, recipientUserIdOffset);
SendRequest.addRecipientRaceName(builder, recipientRaceNameOffset);
SendRequest.addSubject(builder, subjectOffset);
SendRequest.addBody(builder, bodyOffset);
return SendRequest.endSendRequest(builder);
}
unpack(): SendRequestT {
return new SendRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.recipientUserId(),
this.recipientRaceName(),
this.subject(),
this.body()
);
}
unpackTo(_o: SendRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.recipientUserId = this.recipientUserId();
_o.recipientRaceName = this.recipientRaceName();
_o.subject = this.subject();
_o.body = this.body();
}
}
export class SendRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public recipientUserId: string|Uint8Array|null = null,
public recipientRaceName: string|Uint8Array|null = null,
public subject: string|Uint8Array|null = null,
public body: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
const body = (this.body !== null ? builder.createString(this.body!) : 0);
return SendRequest.createSendRequest(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
recipientUserId,
recipientRaceName,
subject,
body
);
}
}
@@ -0,0 +1,77 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
export class SendResponse implements flatbuffers.IUnpackableObject<SendResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SendResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse {
return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
message(obj?:MailMessage):MailMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startSendResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, messageOffset, 0);
}
static endSendResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createSendResponse(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset):flatbuffers.Offset {
SendResponse.startSendResponse(builder);
SendResponse.addMessage(builder, messageOffset);
return SendResponse.endSendResponse(builder);
}
unpack(): SendResponseT {
return new SendResponseT(
(this.message() !== null ? this.message()!.unpack() : null)
);
}
unpackTo(_o: SendResponseT): void {
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
}
}
export class SendResponseT implements flatbuffers.IGeneratedObject {
constructor(
public message: MailMessageT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const message = (this.message !== null ? this.message!.pack(builder) : 0);
return SendResponse.createSendResponse(builder,
message
);
}
}
@@ -0,0 +1,76 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { UUID, UUIDT } from '../common/uuid.js';
export class SentRequest implements flatbuffers.IUnpackableObject<SentRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SentRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest {
return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
gameId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startSentRequest(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static endSentRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
return offset;
}
static createSentRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
SentRequest.startSentRequest(builder);
SentRequest.addGameId(builder, gameIdOffset);
return SentRequest.endSentRequest(builder);
}
unpack(): SentRequestT {
return new SentRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null)
);
}
unpackTo(_o: SentRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
}
}
export class SentRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return SentRequest.createSentRequest(builder,
(this.gameId !== null ? this.gameId!.pack(builder) : 0)
);
}
}
@@ -0,0 +1,94 @@
// automatically generated by the FlatBuffers compiler, do not modify
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import * as flatbuffers from 'flatbuffers';
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
export class SentResponse implements flatbuffers.IUnpackableObject<SentResponseT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):SentResponse {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse {
return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
items(index: number, obj?:MailMessage):MailMessage|null {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
itemsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startSentResponse(builder:flatbuffers.Builder) {
builder.startObject(1);
}
static addItems(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, itemsOffset, 0);
}
static createItemsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
builder.startVector(4, data.length, 4);
for (let i = data.length - 1; i >= 0; i--) {
builder.addOffset(data[i]!);
}
return builder.endVector();
}
static startItemsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endSentResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createSentResponse(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset):flatbuffers.Offset {
SentResponse.startSentResponse(builder);
SentResponse.addItems(builder, itemsOffset);
return SentResponse.endSentResponse(builder);
}
unpack(): SentResponseT {
return new SentResponseT(
this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength())
);
}
unpackTo(_o: SentResponseT): void {
_o.items = this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength());
}
}
export class SentResponseT implements flatbuffers.IGeneratedObject {
constructor(
public items: (MailMessageT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const items = SentResponse.createItemsVector(builder, builder.createObjectOffsetList(this.items));
return SentResponse.createSentResponse(builder,
items
);
}
}
@@ -96,6 +96,7 @@ fresh.
type VerifiedEvent,
} from "../../../api/events.svelte";
import { toast } from "$lib/toast.svelte";
import { mailStore } from "$lib/mail-store.svelte";
let { children } = $props();
@@ -241,6 +242,7 @@ fresh.
// `currentTurn` is known cannot misfire.
let unsubTurnReady: (() => void) | null = null;
let unsubGamePaused: (() => void) | null = null;
let unsubMailReceived: (() => void) | null = null;
const turnReadyDecoder = new TextDecoder("utf-8");
function parseTurnReadyPayload(
@@ -268,6 +270,32 @@ fresh.
}
}
function parseMailReceivedPayload(
event: VerifiedEvent,
): { gameId: string; from: string } | null {
try {
const text = turnReadyDecoder.decode(event.payloadBytes);
const json: unknown = JSON.parse(text);
if (typeof json !== "object" || json === null) {
return null;
}
const record = json as Record<string, unknown>;
const eventGameId = record.game_id;
if (typeof eventGameId !== "string") {
return null;
}
const subject =
typeof record.subject === "string" && record.subject !== ""
? record.subject
: typeof record.preview === "string"
? record.preview
: "";
return { gameId: eventGameId, from: subject };
} catch {
return null;
}
}
function parseGamePausedPayload(
event: VerifiedEvent,
): { gameId: string; reason: string } | null {
@@ -408,9 +436,29 @@ fresh.
}
orderDraft.markPaused({ reason: parsed.reason });
});
unsubMailReceived = eventStream.on(
"diplomail.message.received",
(event) => {
const parsed = parseMailReceivedPayload(event);
if (parsed === null || parsed.gameId !== gameId) {
return;
}
void mailStore.applyPushEvent(parsed.gameId);
toast.show({
messageKey: "game.events.mail_new.message",
messageParams: { from: parsed.from },
actionLabelKey: "game.events.mail_new.action",
onAction: () => {
void goto(`/games/${gameId}/mail`);
},
durationMs: 8000,
});
},
);
await Promise.all([
gameState.init({ client, cache, gameId }),
orderDraft.init({ cache, gameId }),
mailStore.init({ client, cache, gameId }),
]);
galaxyClient.set(client);
orderDraft.bindClient(client, {
@@ -442,6 +490,10 @@ fresh.
unsubGamePaused();
unsubGamePaused = null;
}
if (unsubMailReceived !== null) {
unsubMailReceived();
unsubMailReceived = null;
}
gameState.dispose();
orderDraft.dispose();
selection.dispose();
+125
View File
@@ -0,0 +1,125 @@
// Phase 28 — MailStore threading projection tests. Exercises the
// `entries` derived rune end-to-end with handcrafted inbox + sent
// fixtures; the network surface (GalaxyClient) is left null since
// these tests do not call init / setGame.
import { describe, expect, it } from "vitest";
import {
MailStore,
type MailListEntry,
type MailStandalone,
type MailThread,
} from "../src/lib/mail-store.svelte";
import type { MailMessage } from "../src/api/diplomail";
function makeMessage(overrides: Partial<MailMessage>): MailMessage {
return {
messageId: overrides.messageId ?? crypto.randomUUID(),
gameId: overrides.gameId ?? "00000000-0000-0000-0000-000000000001",
gameName: overrides.gameName ?? "Test game",
kind: overrides.kind ?? "personal",
senderKind: overrides.senderKind ?? "player",
senderUserId: overrides.senderUserId ?? null,
senderUsername: overrides.senderUsername ?? null,
senderRaceName: overrides.senderRaceName ?? null,
subject: overrides.subject ?? "",
body: overrides.body ?? "",
bodyLang: overrides.bodyLang ?? "en",
broadcastScope: overrides.broadcastScope ?? "single",
createdAt: overrides.createdAt ?? new Date(0),
recipientUserId:
overrides.recipientUserId ?? "00000000-0000-0000-0000-000000000099",
recipientUserName: overrides.recipientUserName ?? "self",
recipientRaceName: overrides.recipientRaceName ?? null,
readAt: overrides.readAt ?? null,
deletedAt: overrides.deletedAt ?? null,
translatedSubject: overrides.translatedSubject ?? null,
translatedBody: overrides.translatedBody ?? null,
translationLang: overrides.translationLang ?? null,
translator: overrides.translator ?? null,
};
}
describe("MailStore.entries", () => {
it("groups inbox + sent messages into per-race threads", () => {
const store = new MailStore();
const greysIncoming = makeMessage({
messageId: "m1",
senderKind: "player",
senderRaceName: "Greys",
body: "hi",
createdAt: new Date("2026-05-15T12:00:00Z"),
});
const greysOutgoing = makeMessage({
messageId: "m2",
senderKind: "player",
senderRaceName: "Self",
recipientRaceName: "Greys",
body: "reply",
createdAt: new Date("2026-05-15T12:05:00Z"),
});
store.inbox = [greysIncoming];
store.sent = [greysOutgoing];
const entries = store.entries;
expect(entries.length).toBe(1);
const entry = entries[0];
expect(entry.kind).toBe("thread");
const thread = entry as MailThread;
expect(thread.raceName).toBe("Greys");
expect(thread.messages.map((m) => m.messageId)).toEqual(["m1", "m2"]);
});
it("surfaces system and admin messages as stand-alone items", () => {
const store = new MailStore();
const system = makeMessage({
messageId: "sys-1",
kind: "admin",
senderKind: "system",
subject: "game.paused: details",
createdAt: new Date("2026-05-15T13:00:00Z"),
});
const admin = makeMessage({
messageId: "adm-1",
kind: "admin",
senderKind: "admin",
createdAt: new Date("2026-05-15T13:05:00Z"),
});
store.inbox = [system, admin];
const entries: MailListEntry[] = store.entries;
expect(entries.every((e) => e.kind === "standalone")).toBe(true);
const standalones = entries as MailStandalone[];
expect(standalones.map((e) => e.message.messageId).sort()).toEqual(
["adm-1", "sys-1"],
);
});
it("treats the local player's broadcasts as stand-alone outgoing", () => {
const store = new MailStore();
const broadcast = makeMessage({
messageId: "br-1",
senderKind: "player",
senderRaceName: "Self",
recipientRaceName: "Greys",
broadcastScope: "game_broadcast",
createdAt: new Date("2026-05-15T14:00:00Z"),
});
store.sent = [broadcast];
const entries = store.entries;
expect(entries.length).toBe(1);
expect(entries[0].kind).toBe("standalone");
});
it("counts unread incoming messages in unreadCount", () => {
const store = new MailStore();
store.inbox = [
makeMessage({ readAt: null, createdAt: new Date(1) }),
makeMessage({ readAt: new Date(2), createdAt: new Date(2) }),
makeMessage({ readAt: null, createdAt: new Date(3) }),
];
expect(store.unreadCount).toBe(2);
});
});