23 Commits

Author SHA1 Message Date
Ilia Denisov 49f614926a KNOWN-ISSUES: park sandbox-cancel; owner rejected host-side hypotheses
After the live investigation, the project owner confirms that none
of the host-side cleanup paths apply: no docker prune cron, no
manual `docker rm`, no `dockerd` restart in the window, and the
engine binary does not crash while idling on API calls.

Replace the host-side hypothesis list with a one-line note that
they were considered and rejected, narrow the open suspicion to
the `dev-deploy.yaml` job sequence (`docker build` + `docker
compose build` + the alpine `docker run --rm` for UI seeding +
`docker compose up -d --wait --remove-orphans`), and park the
entry. Reopen if the symptom recurs with a fresh
`docker events --since 0` capture armed before the deploy
starts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:16:51 +02:00
Ilia Denisov cadb72b412 KNOWN-ISSUES: rule out compose orphan reap; narrow to host-side reap
Tests · UI / test (push) Successful in 2m36s
Tests · Go / test (push) Successful in 2m38s
A live `docker inspect` of an engine container and two redispatch
runs with `docker events` captured confirm:

- Engine has no `com.docker.compose.*` labels and `AutoRemove=false`,
  so `--remove-orphans` cannot reap it.
- Two consecutive `dev-deploy.yaml` redispatches with an engine
  already running emitted `die` / `destroy` events only for
  `galaxy-dev-{backend,api,caddy}` — never for the engine.
- The reconciler tick that fires 60s after backend recreate
  correctly matched the surviving engine in both cases
  (`status=running` in both `games` and `runtime_records`).
- `runtime.Service` has no `Shutdown` that proactively removes
  engine containers, so a graceful backend exit also leaves them
  alone.

The repro window therefore needs a separate trigger that removed
the engine container outside of compose. The new hypotheses point
at host-side `docker prune` jobs, a `dockerd` restart that lost the
container, or an early `Engine.Init` failure that exited the engine
before `status=running` reached the runtime row. The investigation
list now leads with `journalctl -u docker` and the host crontab —
those are the cheapest checks to confirm or rule out next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:10:13 +02:00
Ilia Denisov 5177fef2ef tools/dev-deploy: log the sandbox-cancellation TODO
Capture the diagnostic notes for the issue we hit after every
`dev-deploy.yaml` redispatch: the freshly-bootstrapped "Dev Sandbox"
game ends up `cancelled` ~15 minutes later, with the runtime
reconciler reporting "container disappeared". The engine never
shows up in `docker ps -a --filter label=galaxy-game-engine`, so
either it never spawned or it was removed before any host-side
snapshot.

`KNOWN-ISSUES.md` records the symptom, the log excerpt, three
working hypotheses (runtime spawn race, `--remove-orphans`
interaction, engine `--rm` lifecycle), and the investigation
checklist before opening an issue. The README gets a one-line
pointer so future redeploys land on the doc immediately.

No code change — this is the placeholder so the next person
investigating the cancellation pattern does not have to
rediscover the diagnostic from scratch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:56:25 +02:00
developer 823be9d980 Phase 28: diplomatic mail UI
Deploy · Dev / deploy (push) Successful in 29s
Tests · Go / test (push) Successful in 2m5s
Tests · Integration / integration (push) Successful in 1m45s
Tests · UI / test (push) Successful in 2m31s
Merges feat/ui-stage-28 into development.
2026-05-16 20:56:16 +00:00
Ilia Denisov 2119f825d6 mail UI: dedupe broadcast fan-out and drop in-game admin compose
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m6s
Tests · UI / test (pull_request) Successful in 2m24s
Two issues surfaced once the long-lived dev environment finally
reached the diplomail view:

1. `/sent` returns one row per recipient for broadcast and admin
   fan-outs (so the admin tooling can render the materialised
   audience). The list pane fed all rows into the stand-alone
   bucket, so the `{#each entries as e (entryKey(e))}` key in
   `thread-list.svelte` collapsed to the same `standalone:${id}`
   for every recipient and Svelte 5 aborted the render with
   `each_key_duplicate`. Dedupe stand-alones by `message_id` in
   `buildEntries`.

2. The compose dialog exposed an `admin` kind toggle gated on
   "owner of game". That was a Phase 28 plan decision, but admin
   compose is an operator tool (server admin), not an in-game
   action — every game owner should not be able to broadcast
   admin notifications. Drop the admin option, the audience
   sub-toggles, and the admin path through `submit`. The
   `MailStore.composeAdmin` wrapper and the backend RPC stay so
   the future admin UI can call them.

Vitest covers the fan-out dedup with three rows sharing one
`message_id` collapsing to a single stand-alone entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:38:59 +02:00
Ilia Denisov 57e6c1d253 gateway: CORS allow-list for the authenticated Connect-Web surface
Tests · Go / test (push) Successful in 2m9s
Tests · Go / test (pull_request) Successful in 2m9s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · UI / test (pull_request) Successful in 2m52s
The public REST listener already exposes
`GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS`; the authenticated
Connect-Web listener on the separate gRPC port had no equivalent.
That worked in `tools/local-dev` (Vite proxy makes everything
same-origin) and would work in production once UI and gateway share
a single hostname, but the long-lived dev environment serves the
UI from `https://www.galaxy.lan` and the gateway from
`https://api.galaxy.lan` — every `/galaxy.gateway.v1.EdgeGateway/*`
fetch failed in the browser with the WebKit "Load failed" generic
message because the response carried no `Access-Control-Allow-Origin`
header. Lobby rendered as "[unknown] Load failed" with no game.

Mirror the public-REST CORS surface for the authenticated handler:

- new env `GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS`;
- new `AuthenticatedGRPCConfig.CORSAllowedOrigins` field;
- new `grpcapi.withCORS` middleware wrapping the Connect mux;
- dev-deploy stack sets the env to `https://www.galaxy.lan`.

The middleware speaks plain net/http (the Connect handler is mounted
on a ServeMux, not gin), handles preflight 204 immediately, and
exposes the Connect-Web header set the browser needs to read the
response (`Grpc-Status`, `Grpc-Message`, `Connect-Protocol-Version`).
Empty allow-list disables the middleware — production stays at
"single hostname" by default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:15:11 +02:00
Ilia Denisov 4b2a949f12 dev-deploy Caddy: route Connect-Web traffic to gateway :9090
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · Go / test (pull_request) Successful in 2m6s
Tests · UI / test (pull_request) Successful in 2m27s
`api.galaxy.lan` was proxying every path to `galaxy-api:8080` (the
public REST listener), so authenticated Connect-Web calls
(`/galaxy.gateway.v1.EdgeGateway/ExecuteCommand`,
`/galaxy.gateway.v1.EdgeGateway/SubscribeEvents`) collapsed to a 404
from the public route table — the lobby loaded the static bundle
but every authenticated query failed silently.

Split routing by path: `/galaxy.gateway.v1.EdgeGateway/*` goes to
the authenticated listener on `:9090`, everything else stays on
`:8080`. Mirrors the Vite dev-server proxy in
`ui/frontend/vite.config.ts`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:03:55 +02:00
Ilia Denisov 81917acc3e dev-deploy: enable Dev Sandbox bootstrap and synthetic-report loader
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · Go / test (pull_request) Successful in 2m4s
Tests · UI / test (pull_request) Successful in 2m23s
Two long-standing dev-environment ergonomics had not survived the
move from the bespoke local-dev stack to the CI-driven dev-deploy:

1. `BACKEND_DEV_SANDBOX_EMAIL` defaulted to an empty string in the
   dev-deploy compose, so the auto-provisioned "Dev Sandbox" game
   never appeared on `https://www.galaxy.lan`. Bake `dev@galaxy.lan`
   as the default — matches `.env.example` and lets a developer who
   logs in with that email find a ready-to-play game in the lobby.

2. The lobby's synthetic-report loader was gated on
   `import.meta.env.DEV`, which is true only for `vite dev` (the
   tools/local-dev path). The long-lived dev environment builds
   with `vite build` (production mode), so the section was always
   stripped from its bundle. Gate it on an explicit
   `VITE_GALAXY_DEV_AFFORDANCES` flag instead and set it both in
   `.env.development` (preserves `pnpm dev` behaviour) and in the
   `dev-deploy.yaml` build step. The `prod-build.yaml` build path
   leaves the flag unset, so production stays clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:46:24 +02:00
Ilia Denisov 859b157a59 auth dev-fixed-code bypasses attempts cap; dev-deploy gains manual dispatch
Tests · Go / test (pull_request) Successful in 2m9s
Tests · Go / test (push) Successful in 2m9s
Tests · Integration / integration (pull_request) Successful in 1m49s
Tests · UI / test (pull_request) Successful in 2m51s
Two problems showed up while trying to log into the long-lived dev
environment with the dev-fixed code `123456`:

1. `ConfirmEmailCode` checked the per-challenge attempts ceiling
   *before* the dev-fixed-code override. A developer who burned past
   `ChallengeMaxAttempts` on an existing un-consumed challenge (easy
   to trigger when the throttle reuses one challenge_id) hit
   `ErrTooManyAttempts` and the UI rendered "code expired or already
   used" even though the fixed code was correct. Reorder so the
   dev-fixed-code branch runs first and bypasses both the bcrypt
   verify and the attempts gate. Production stays unaffected
   because production loaders refuse to set `DevFixedCode`.

2. `dev-deploy.yaml` only fires on push to `development`, so the
   matching docker-compose default change for
   `BACKEND_AUTH_DEV_FIXED_CODE` could not reach the running stack
   before this PR merged. Add `workflow_dispatch: {}` so a developer
   can deploy any branch — typically a feature branch under review —
   from the Gitea Actions UI without waiting for the merge.

Covered by a new `TestConfirmEmailCodeDevFixedCodeBypassesAttemptsCeiling`
integration test that burns through the ceiling with wrong codes
then proves the dev-fixed code still produces a session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:28:30 +02:00
Ilia Denisov 166baf4be0 battle-viewer e2e: mock user.games.battle ConnectRPC command
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m0s
Tests · UI / test (pull_request) Successful in 2m23s
Phase 28 moved the battle fetch off the REST passthrough onto the
signed envelope, so the Playwright spec's `page.route(...)` against
the old REST path no longer intercepts anything and the viewer
times out waiting for data. Update the spec to:

- Build a FlatBuffers `BattleReport` payload in
  `fixtures/battle-fbs.ts` (mirrors `report-fbs.ts`'s pattern).
- Add a `user.games.battle` case to the ExecuteCommand mock that
  decodes the FBS `GameBattleRequest`, returns the encoded report
  when the battle_id matches the seeded one, and surfaces a
  canonical `not_found` resultCode otherwise.
- Drop the obsolete REST route stubs.
- Drive the negative-path test with a real UUID that does not match
  the seeded one, so the gateway-side switch is the source of the
  404 (the old `missing-uuid` literal was no longer a valid wire
  shape for the UUID decoder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:55:15 +02:00
Ilia Denisov ebd156ece2 battle-fetch: migrate to user.games.battle ConnectRPC command
Tests · UI / test (push) Has been cancelled
Tests · Go / test (pull_request) Successful in 2m6s
Tests · Go / test (push) Successful in 2m7s
Tests · Integration / integration (pull_request) Successful in 1m47s
Tests · UI / test (pull_request) Failing after 3m42s
The Phase 27 BattleViewer was the last UI surface still issuing raw
fetch() against the backend REST contract (`/api/v1/user/games/...
/battles/...`). The dev-deploy gateway never proxied that path, so
the viewer worked only in tools/local-dev/. Move it onto the signed
ConnectRPC channel every other authenticated surface already uses.

Wire pieces:
- FBS GameBattleRequest in pkg/schema/fbs/battle.fbs, regenerated
  Go + TS bindings.
- MessageTypeUserGamesBattle constant + GameBattleRequest struct in
  pkg/model/report/messages.go.
- pkg/transcoder/battle.go gains GameBattleRequestToPayload and
  PayloadToGameBattleRequest helpers.
- gateway games_commands.go switches on the new message type and
  GETs /api/v1/user/games/{id}/battles/{turn}/{battle_id}; the JSON
  response is re-encoded as a FlatBuffers BattleReport before being
  returned. 404 from backend surfaces as the canonical `not_found`
  gateway error.
- ui/frontend/src/api/battle-fetch.ts now builds the FBS request,
  calls GalaxyClient.executeCommand, and decodes the FBS response
  into the existing UI shape (Record<string,string> race/ship maps,
  string-form UUID). BattleFetchError carries an HTTP-style status
  derived from the result code so the active-view's not_found branch
  keeps working.
- battle.svelte pulls the GalaxyClient from the in-game shell
  context. While the layout's boot Promise.all is in flight the
  effect stays in `loading` until the client handle becomes
  non-null.
- ui/Makefile FBS_INPUTS gains battle.fbs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:41:54 +02:00
Ilia Denisov 8bc75fd71b dev-deploy: default BACKEND_AUTH_DEV_FIXED_CODE to 123456
The long-lived dev environment now opts into the bcrypt-bypass on a
fresh `up`/`rebuild` so a returning developer can sign in with `123456`
even after the matching browser session was cleared (the real emailed
code is single-use). Set the variable to an empty string in `.env` to
force real Mailpit codes (mail-flow QA).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:41:32 +02:00
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
109 changed files with 9928 additions and 266 deletions
+12
View File
@@ -7,6 +7,12 @@ name: Deploy · Dev
# `integration` as part of the PR that produced this push, so this # `integration` as part of the PR that produced this push, so this
# workflow does not re-run those tests — it focuses on packaging and # workflow does not re-run those tests — it focuses on packaging and
# rollout. # rollout.
#
# `workflow_dispatch` is also accepted so a developer can deploy any
# branch (typically a feature branch under active review) into the
# shared dev environment from the Gitea Actions UI without waiting for
# the PR to merge first. The deploy job picks up whatever the chosen
# ref is — same packaging + healthcheck steps as the merge path.
on: on:
push: push:
@@ -23,6 +29,7 @@ on:
- 'tools/dev-deploy/**' - 'tools/dev-deploy/**'
- '.gitea/workflows/dev-deploy.yaml' - '.gitea/workflows/dev-deploy.yaml'
- '!**/*.md' - '!**/*.md'
workflow_dispatch: {}
jobs: jobs:
deploy: deploy:
@@ -62,6 +69,11 @@ jobs:
working-directory: ui/frontend working-directory: ui/frontend
env: env:
VITE_GATEWAY_BASE_URL: https://api.galaxy.lan VITE_GATEWAY_BASE_URL: https://api.galaxy.lan
# Surface the synthetic-report loader and similar dev-only
# affordances in the long-lived dev bundle. The prod build
# path (`prod-build.yaml`) leaves this flag unset so the
# production bundle keeps the same affordances stripped.
VITE_GALAXY_DEV_AFFORDANCES: "true"
run: | run: |
# The response-signing public key is committed in # The response-signing public key is committed in
# `.env.development` alongside its private counterpart in # `.env.development` alongside its private counterpart in
+46
View File
@@ -513,6 +513,52 @@ func TestConfirmEmailCodeWrongCode(t *testing.T) {
} }
} }
// TestConfirmEmailCodeDevFixedCodeBypassesAttemptsCeiling proves the
// dev-mode override is a true escape hatch: a developer who already
// burned past ChallengeMaxAttempts on a long-lived dev challenge
// (typically because the throttle merged repeated send-email-code
// calls onto one challenge_id) can still recover by submitting the
// fixed code without first waiting out the challenge TTL.
func TestConfirmEmailCodeDevFixedCodeBypassesAttemptsCeiling(t *testing.T) {
db := startPostgres(t)
cfg := authConfig()
cfg.DevFixedCode = "999999"
svc := buildServiceWithConfig(t, db, cfg)
ctx := context.Background()
id, err := svc.SendEmailCode(ctx, "dev-bypass-ceiling@example.test", "en", "", "")
if err != nil {
t.Fatalf("send: %v", err)
}
// Burn through the attempts ceiling with deliberately wrong codes.
for i := range cfg.ChallengeMaxAttempts + 1 {
_, err := svc.ConfirmEmailCode(ctx, auth.ConfirmInputs{
ChallengeID: id,
Code: "111111",
ClientPublicKey: randomKey(t),
TimeZone: "UTC",
})
if err == nil {
t.Fatalf("attempt %d unexpectedly succeeded", i)
}
}
// The dev-fixed code still goes through.
session, err := svc.ConfirmEmailCode(ctx, auth.ConfirmInputs{
ChallengeID: id,
Code: "999999",
ClientPublicKey: randomKey(t),
TimeZone: "UTC",
})
if err != nil {
t.Fatalf("dev-fixed-code after attempts exhausted: %v", err)
}
if session.DeviceSessionID == uuid.Nil {
t.Fatalf("dev-fixed-code did not produce a session")
}
}
func TestConfirmEmailCodeAttemptsCeiling(t *testing.T) { func TestConfirmEmailCodeAttemptsCeiling(t *testing.T) {
db := startPostgres(t) db := startPostgres(t)
svc, mailer, _, _ := buildService(t, db) svc, mailer, _, _ := buildService(t, db)
+15 -6
View File
@@ -163,6 +163,21 @@ func (s *Service) ConfirmEmailCode(ctx context.Context, in ConfirmInputs) (Sessi
return Session{}, err return Session{}, err
} }
// The dev-mode fixed-code override is checked first so it bypasses
// both the bcrypt verify and the per-challenge attempts ceiling.
// Without this, a developer who already burned through
// `ChallengeMaxAttempts` on an existing un-consumed challenge —
// for example after the throttle merged repeated send-email-code
// calls onto one challenge_id — could not recover with the fixed
// code either, defeating the purpose of the override. Production
// deployments leave `DevFixedCode` empty, so this branch is
// inert and the regular attempts gate still applies.
if s.devFixedCodeMatches(in.Code) {
s.deps.Logger.Warn("auth challenge accepted via dev-mode fixed code override",
zap.String("challenge_id", in.ChallengeID.String()),
zap.Int32("attempts", loaded.Attempts),
)
} else {
if int(loaded.Attempts) > s.deps.Config.ChallengeMaxAttempts { if int(loaded.Attempts) > s.deps.Config.ChallengeMaxAttempts {
s.deps.Logger.Info("auth challenge attempts exhausted", s.deps.Logger.Info("auth challenge attempts exhausted",
zap.String("challenge_id", in.ChallengeID.String()), zap.String("challenge_id", in.ChallengeID.String()),
@@ -170,8 +185,6 @@ func (s *Service) ConfirmEmailCode(ctx context.Context, in ConfirmInputs) (Sessi
) )
return Session{}, ErrTooManyAttempts return Session{}, ErrTooManyAttempts
} }
if !s.devFixedCodeMatches(in.Code) {
if err := verifyCode(loaded.CodeHash, in.Code); err != nil { if err := verifyCode(loaded.CodeHash, in.Code); err != nil {
if errors.Is(err, ErrCodeMismatch) { if errors.Is(err, ErrCodeMismatch) {
s.deps.Logger.Info("auth challenge code mismatch", s.deps.Logger.Info("auth challenge code mismatch",
@@ -182,10 +195,6 @@ func (s *Service) ConfirmEmailCode(ctx context.Context, in ConfirmInputs) (Sessi
} }
return Session{}, err return Session{}, err
} }
} else {
s.deps.Logger.Warn("auth challenge accepted via dev-mode fixed code override",
zap.String("challenge_id", in.ChallengeID.String()),
)
} }
// Re-check permanent_block after verifying the code. SendEmailCode // Re-check permanent_block after verifying the code. SendEmailCode
+22 -1
View File
@@ -26,7 +26,10 @@ Three Postgres tables in the `backend` schema:
- `diplomail_messages` — one row per send (personal, admin, or - `diplomail_messages` — one row per send (personal, admin, or
system). Captures `game_name` and IP at insert time so audit 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 - `diplomail_recipients` — one row per (message, recipient). Holds
per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at` per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at`
state. Snapshot fields (`recipient_user_name`, state. Snapshot fields (`recipient_user_name`,
@@ -72,6 +75,24 @@ mail to every active member; `Service.changeMembershipStatus` /
`detector.LanguageDetector` (default: `whatlanggo`, body-only, `detector.LanguageDetector` (default: `whatlanggo`, body-only,
≥ 25 runes; shorter bodies stay `und`). ≥ 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 ## Translation
Stage D adds a lazy translation cache. When a recipient reads a 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 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 err != nil {
if errors.Is(err, ErrNotFound) { if errors.Is(err, ErrNotFound) {
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not a member of the game", ErrForbidden) 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) 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) recipient.GameID, recipient.GameName, subject, body, in.SenderIP, BroadcastScopeSingle)
if err != nil { if err != nil {
return Message{}, Recipient{}, err return Message{}, Recipient{}, err
@@ -84,7 +88,7 @@ func (s *Service) SendAdminBroadcast(ctx context.Context, in SendAdminBroadcastI
} }
gameName := members[0].GameName 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) in.GameID, gameName, subject, body, in.SenderIP, BroadcastScopeGameBroadcast)
if err != nil { if err != nil {
return Message{}, nil, err return Message{}, nil, err
@@ -147,6 +151,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
} }
username := sender.UserName username := sender.UserName
senderRace := sender.RaceName
msgInsert := MessageInsert{ msgInsert := MessageInsert{
MessageID: uuid.New(), MessageID: uuid.New(),
GameID: in.GameID, GameID: in.GameID,
@@ -155,6 +160,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
SenderKind: SenderKindPlayer, SenderKind: SenderKindPlayer,
SenderUserID: &callerID, SenderUserID: &callerID,
SenderUsername: &username, SenderUsername: &username,
SenderRaceName: &senderRace,
SenderIP: in.SenderIP, SenderIP: in.SenderIP,
Subject: subject, Subject: subject,
Body: body, Body: body,
@@ -217,7 +223,7 @@ func (s *Service) SendAdminMultiGameBroadcast(ctx context.Context, in SendMultiG
zap.String("scope", scope)) zap.String("scope", scope))
continue 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) game.GameID, game.GameName, subject, body, in.SenderIP, BroadcastScopeMultiGameBroadcast)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -356,7 +362,7 @@ func (s *Service) publishGameLifecycle(ctx context.Context, ev LifecycleEvent) e
gameName := members[0].GameName gameName := members[0].GameName
subject, body := renderGameLifecycle(ev.Kind, gameName, ev.Actor, ev.Reason) 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) ev.GameID, gameName, subject, body, "", BroadcastScopeGameBroadcast)
if err != nil { if err != nil {
return err 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) 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) ev.GameID, target.GameName, subject, body, "", BroadcastScopeSingle)
if err != nil { if err != nil {
return err 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 // for every admin-kind send. The CHECK constraint maps sender
// shapes: // 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='admin' → CallerKind admin; sender_user_id nil
// sender_kind='system' → CallerKind system; sender_username 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) { gameID uuid.UUID, gameName, subject, body, senderIP, scope string) (MessageInsert, error) {
out := MessageInsert{ out := MessageInsert{
MessageID: uuid.New(), MessageID: uuid.New(),
@@ -443,6 +451,17 @@ func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.
out.SenderKind = SenderKindPlayer out.SenderKind = SenderKindPlayer
out.SenderUserID = &uid out.SenderUserID = &uid
out.SenderUsername = &uname 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: case CallerKindAdmin:
uname := callerUsername uname := callerUsername
out.SenderKind = SenderKindAdmin 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) { func TestDiplomailRejectsNonActiveSender(t *testing.T) {
db := startPostgres(t) db := startPostgres(t)
ctx := context.Background() ctx := context.Background()
+62 -10
View File
@@ -32,15 +32,20 @@ const previewMaxRunes = 120
// ErrForbidden; the inserted Message is never persisted in those // ErrForbidden; the inserted Message is never persisted in those
// cases. // cases.
func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Message, Recipient, error) { 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") subject := strings.TrimRight(in.Subject, " \t")
body := strings.TrimRight(in.Body, " \t\n") body := strings.TrimRight(in.Body, " \t\n")
if err := s.validateContent(subject, body); err != nil { if err := s.validateContent(subject, body); err != nil {
return Message{}, Recipient{}, err 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) sender, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.SenderUserID)
if err != nil { if err != nil {
if errors.Is(err, ErrNotFound) { 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) 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 err != nil {
if errors.Is(err, ErrNotFound) { if errors.Is(err, ErrNotFound) {
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not an active member of the game", ErrForbidden) 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 username := sender.UserName
senderRace := sender.RaceName
senderUserID := in.SenderUserID
msgInsert := MessageInsert{ msgInsert := MessageInsert{
MessageID: uuid.New(), MessageID: uuid.New(),
GameID: in.GameID, GameID: in.GameID,
GameName: sender.GameName, GameName: sender.GameName,
Kind: KindPersonal, Kind: KindPersonal,
SenderKind: SenderKindPlayer, SenderKind: SenderKindPlayer,
SenderUserID: &in.SenderUserID, SenderUserID: &senderUserID,
SenderUsername: &username, SenderUsername: &username,
SenderRaceName: &senderRace,
SenderIP: in.SenderIP, SenderIP: in.SenderIP,
Subject: subject, Subject: subject,
Body: body, Body: body,
@@ -75,7 +83,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
rcptInsert := buildRecipientInsert( rcptInsert := buildRecipientInsert(
msgInsert.MessageID, msgInsert.MessageID,
MemberSnapshot{ MemberSnapshot{
UserID: in.RecipientUserID, UserID: recipientID,
GameID: in.GameID, GameID: in.GameID,
GameName: recipient.GameName, GameName: recipient.GameName,
UserName: recipient.UserName, UserName: recipient.UserName,
@@ -101,6 +109,47 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
return msg, recipients[0], nil 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 // GetMessage returns the InboxEntry for messageID addressed to
// userID. ErrNotFound is returned when the caller is not a recipient // userID. ErrNotFound is returned when the caller is not a recipient
// of the message — handlers translate that to 404 so the existence // 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 return map[string]bool{KindAdmin: true}, nil
} }
// ListSent returns personal messages authored by senderUserID in // ListSent returns the sender-side view of personal messages
// gameID, newest first. Admin/system rows have no `sender_user_id` // authored by senderUserID in gameID, newest first. Each entry pairs
// and are therefore excluded; the user surface does not need them. // the message with one of its recipient rows; single sends contribute
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) { // 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) return s.deps.Store.ListSent(ctx, gameID, senderUserID)
} }
+33 -14
View File
@@ -31,7 +31,7 @@ func messageColumns() postgres.ColumnList {
m := table.DiplomailMessages m := table.DiplomailMessages
return postgres.ColumnList{ return postgres.ColumnList{
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind, 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, m.Subject, m.Body, m.BodyLang, m.BroadcastScope, m.CreatedAt,
} }
} }
@@ -59,6 +59,7 @@ type MessageInsert struct {
SenderKind string SenderKind string
SenderUserID *uuid.UUID SenderUserID *uuid.UUID
SenderUsername *string SenderUsername *string
SenderRaceName *string
SenderIP string SenderIP string
Subject string Subject string
Body string Body string
@@ -101,7 +102,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
m := table.DiplomailMessages m := table.DiplomailMessages
msgStmt := m.INSERT( msgStmt := m.INSERT(
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind, 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.Subject, m.Body, m.BodyLang, m.BroadcastScope,
).VALUES( ).VALUES(
msg.MessageID, msg.MessageID,
@@ -111,6 +112,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
msg.SenderKind, msg.SenderKind,
uuidPtrArg(msg.SenderUserID), uuidPtrArg(msg.SenderUserID),
stringPtrArg(msg.SenderUsername), stringPtrArg(msg.SenderUsername),
stringPtrArg(msg.SenderRaceName),
msg.SenderIP, msg.SenderIP,
msg.Subject, msg.Subject,
msg.Body, msg.Body,
@@ -241,25 +243,38 @@ func (s *Store) ListInbox(ctx context.Context, gameID, userID uuid.UUID) ([]Inbo
return out, nil return out, nil
} }
// ListSent returns messages authored by senderUserID in gameID, // ListSent returns the sender-side view of personal messages
// newest first. Personal messages only — admin/system rows have // authored by senderUserID in gameID, newest first. Each
// `sender_user_id IS NULL` and are filtered out by the WHERE clause. // `InboxEntry` carries the message together with one of its
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) { // 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 m := table.DiplomailMessages
stmt := postgres.SELECT(messageColumns()). r := table.DiplomailRecipients
FROM(m). cols := append(messageColumns(), recipientColumns()...)
stmt := postgres.SELECT(cols).
FROM(m.INNER_JOIN(r, r.MessageID.EQ(m.MessageID))).
WHERE( WHERE(
m.GameID.EQ(postgres.UUID(gameID)). m.GameID.EQ(postgres.UUID(gameID)).
AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))), AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))),
). ).
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC()) ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC(), r.RecipientID.ASC())
var rows []model.DiplomailMessages var dest []struct {
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil { 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) return nil, fmt.Errorf("diplomail store: list sent %s/%s: %w", gameID, senderUserID, err)
} }
out := make([]Message, 0, len(rows)) out := make([]InboxEntry, 0, len(dest))
for _, row := range rows { for _, row := range dest {
out = append(out, messageFromModel(row)) out = append(out, InboxEntry{
Message: messageFromModel(row.DiplomailMessages),
Recipient: recipientFromModel(row.Recipient),
})
} }
return out, nil return out, nil
} }
@@ -737,6 +752,10 @@ func messageFromModel(row model.DiplomailMessages) Message {
name := *row.SenderUsername name := *row.SenderUsername
out.SenderUsername = &name out.SenderUsername = &name
} }
if row.SenderRaceName != nil {
name := *row.SenderRaceName
out.SenderRaceName = &name
}
return out return out
} }
+17 -5
View File
@@ -23,6 +23,11 @@ type Message struct {
SenderKind string SenderKind string
SenderUserID *uuid.UUID SenderUserID *uuid.UUID
SenderUsername *string 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 SenderIP string
Subject string Subject string
Body string Body string
@@ -92,13 +97,16 @@ type Translation struct {
} }
// SendPersonalInput is the request payload for SendPersonal: the // SendPersonalInput is the request payload for SendPersonal: the
// caller sending a single-recipient personal message. Validation // caller sending a single-recipient personal message. Exactly one of
// (active membership, body length, etc.) is performed inside the // RecipientUserID and RecipientRaceName must be non-zero; the
// service. // 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 { type SendPersonalInput struct {
GameID uuid.UUID GameID uuid.UUID
SenderUserID uuid.UUID SenderUserID uuid.UUID
RecipientUserID uuid.UUID RecipientUserID uuid.UUID
RecipientRaceName string
Subject string Subject string
Body string Body string
SenderIP string SenderIP string
@@ -116,14 +124,18 @@ const (
// SendAdminPersonalInput is the request payload for an owner / // SendAdminPersonalInput is the request payload for an owner /
// admin / system sending an admin-kind message to a single // admin / system sending an admin-kind message to a single
// recipient. Authorization (owner-vs-admin distinction) is enforced // recipient. Exactly one of RecipientUserID and RecipientRaceName
// by the HTTP layer; the service trusts the caller designation. // 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 { type SendAdminPersonalInput struct {
GameID uuid.UUID GameID uuid.UUID
CallerKind string CallerKind string
CallerUserID *uuid.UUID CallerUserID *uuid.UUID
CallerUsername string CallerUsername string
RecipientUserID uuid.UUID RecipientUserID uuid.UUID
RecipientRaceName string
Subject string Subject string
Body string Body string
SenderIP string SenderIP string
@@ -20,6 +20,7 @@ type DiplomailMessages struct {
SenderKind string SenderKind string
SenderUserID *uuid.UUID SenderUserID *uuid.UUID
SenderUsername *string SenderUsername *string
SenderRaceName *string
SenderIP string SenderIP string
Subject string Subject string
Body string Body string
@@ -24,6 +24,7 @@ type diplomailMessagesTable struct {
SenderKind postgres.ColumnString SenderKind postgres.ColumnString
SenderUserID postgres.ColumnString SenderUserID postgres.ColumnString
SenderUsername postgres.ColumnString SenderUsername postgres.ColumnString
SenderRaceName postgres.ColumnString
SenderIP postgres.ColumnString SenderIP postgres.ColumnString
Subject postgres.ColumnString Subject postgres.ColumnString
Body postgres.ColumnString Body postgres.ColumnString
@@ -78,14 +79,15 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
SenderKindColumn = postgres.StringColumn("sender_kind") SenderKindColumn = postgres.StringColumn("sender_kind")
SenderUserIDColumn = postgres.StringColumn("sender_user_id") SenderUserIDColumn = postgres.StringColumn("sender_user_id")
SenderUsernameColumn = postgres.StringColumn("sender_username") SenderUsernameColumn = postgres.StringColumn("sender_username")
SenderRaceNameColumn = postgres.StringColumn("sender_race_name")
SenderIPColumn = postgres.StringColumn("sender_ip") SenderIPColumn = postgres.StringColumn("sender_ip")
SubjectColumn = postgres.StringColumn("subject") SubjectColumn = postgres.StringColumn("subject")
BodyColumn = postgres.StringColumn("body") BodyColumn = postgres.StringColumn("body")
BodyLangColumn = postgres.StringColumn("body_lang") BodyLangColumn = postgres.StringColumn("body_lang")
BroadcastScopeColumn = postgres.StringColumn("broadcast_scope") BroadcastScopeColumn = postgres.StringColumn("broadcast_scope")
CreatedAtColumn = postgres.TimestampzColumn("created_at") CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{MessageIDColumn, 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, 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} defaultColumns = postgres.ColumnList{SenderIPColumn, SubjectColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
) )
@@ -100,6 +102,7 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
SenderKind: SenderKindColumn, SenderKind: SenderKindColumn,
SenderUserID: SenderUserIDColumn, SenderUserID: SenderUserIDColumn,
SenderUsername: SenderUsernameColumn, SenderUsername: SenderUsernameColumn,
SenderRaceName: SenderRaceNameColumn,
SenderIP: SenderIPColumn, SenderIP: SenderIPColumn,
Subject: SubjectColumn, Subject: SubjectColumn,
Body: BodyColumn, Body: BodyColumn,
@@ -683,6 +683,11 @@ CREATE TABLE diplomail_messages (
sender_kind text NOT NULL, sender_kind text NOT NULL,
sender_user_id uuid, sender_user_id uuid,
sender_username text, 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 '', sender_ip text NOT NULL DEFAULT '',
subject text NOT NULL DEFAULT '', subject text NOT NULL DEFAULT '',
body text NOT NULL, 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 = '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_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 ( CONSTRAINT diplomail_messages_kind_sender_chk CHECK (
(kind = 'personal' AND sender_kind = 'player') OR (kind = 'personal' AND sender_kind = 'player') OR
(kind = 'admin' AND sender_kind IN ('player', 'admin', 'system')) (kind = 'admin' AND sender_kind IN ('player', 'admin', 'system'))
@@ -60,16 +60,21 @@ func (h *AdminDiplomailHandlers) Send() gin.HandlerFunc {
ctx := c.Request.Context() ctx := c.Request.Context()
switch req.Target { switch req.Target {
case "", "user": case "", "user":
recipientID, parseErr := uuid.Parse(req.RecipientUserID) var recipientID uuid.UUID
if req.RecipientUserID != "" {
parsed, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil { if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return return
} }
recipientID = parsed
}
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{ msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
GameID: gameID, GameID: gameID,
CallerKind: diplomail.CallerKindAdmin, CallerKind: diplomail.CallerKindAdmin,
CallerUsername: username, CallerUsername: username,
RecipientUserID: recipientID, RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject, Subject: req.Subject,
Body: req.Body, Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c), SenderIP: clientip.ExtractSourceIP(c),
+36 -40
View File
@@ -87,16 +87,21 @@ func (h *UserMailHandlers) SendPersonal() gin.HandlerFunc {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON")
return return
} }
recipientID, err := uuid.Parse(req.RecipientUserID) var recipientID uuid.UUID
if err != nil { 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") httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return return
} }
recipientID = parsed
}
ctx := c.Request.Context() ctx := c.Request.Context()
msg, rcpt, err := h.svc.SendPersonal(ctx, diplomail.SendPersonalInput{ msg, rcpt, err := h.svc.SendPersonal(ctx, diplomail.SendPersonalInput{
GameID: gameID, GameID: gameID,
SenderUserID: userID, SenderUserID: userID,
RecipientUserID: recipientID, RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject, Subject: req.Subject,
Body: req.Body, Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c), SenderIP: clientip.ExtractSourceIP(c),
@@ -189,9 +194,9 @@ func (h *UserMailHandlers) Sent() gin.HandlerFunc {
respondDiplomailError(c, h.logger, "user mail sent", ctx, err) respondDiplomailError(c, h.logger, "user mail sent", ctx, err)
return return
} }
out := userMailSentListWire{Items: make([]userMailSentSummaryWire, 0, len(items))} out := userMailSentListWire{Items: make([]userMailMessageDetailWire, 0, len(items))}
for _, m := range items { for _, entry := range items {
out.Items = append(out.Items, mailMessageSummaryToWire(m)) out.Items = append(out.Items, mailMessageDetailToWire(entry, false))
} }
c.JSON(http.StatusOK, out) c.JSON(http.StatusOK, out)
} }
@@ -341,11 +346,15 @@ func (h *UserMailHandlers) SendAdmin() gin.HandlerFunc {
switch req.Target { switch req.Target {
case "", "user": case "", "user":
recipientID, parseErr := uuid.Parse(req.RecipientUserID) var recipientID uuid.UUID
if req.RecipientUserID != "" {
parsed, parseErr := uuid.Parse(req.RecipientUserID)
if parseErr != nil { if parseErr != nil {
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
return return
} }
recipientID = parsed
}
callerUserID := userID callerUserID := userID
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{ msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
GameID: gameID, GameID: gameID,
@@ -353,6 +362,7 @@ func (h *UserMailHandlers) SendAdmin() gin.HandlerFunc {
CallerUserID: &callerUserID, CallerUserID: &callerUserID,
CallerUsername: account.UserName, CallerUsername: account.UserName,
RecipientUserID: recipientID, RecipientUserID: recipientID,
RecipientRaceName: req.RecipientRaceName,
Subject: req.Subject, Subject: req.Subject,
Body: req.Body, Body: req.Body,
SenderIP: clientip.ExtractSourceIP(c), SenderIP: clientip.ExtractSourceIP(c),
@@ -449,8 +459,11 @@ func parseMessageIDParam(c *gin.Context) (uuid.UUID, bool) {
} }
// userMailSendRequestWire mirrors the request body for SendPersonal. // 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 { type userMailSendRequestWire struct {
RecipientUserID string `json:"recipient_user_id"` RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Subject string `json:"subject,omitempty"` Subject string `json:"subject,omitempty"`
Body string `json:"body"` Body string `json:"body"`
} }
@@ -464,12 +477,13 @@ type userMailSendBroadcastRequestWire struct {
} }
// userMailSendAdminRequestWire mirrors the request body for the // userMailSendAdminRequestWire mirrors the request body for the
// owner-only admin send. `target="user"` requires // owner-only admin send. `target="user"` requires exactly one of
// `recipient_user_id`; `target="all"` accepts the optional // `recipient_user_id` and `recipient_race_name`; `target="all"`
// `recipients` scope (default `active`). // accepts the optional `recipients` scope (default `active`).
type userMailSendAdminRequestWire struct { type userMailSendAdminRequestWire struct {
Target string `json:"target"` Target string `json:"target"`
RecipientUserID string `json:"recipient_user_id,omitempty"` RecipientUserID string `json:"recipient_user_id,omitempty"`
RecipientRaceName string `json:"recipient_race_name,omitempty"`
Recipients string `json:"recipients,omitempty"` Recipients string `json:"recipients,omitempty"`
Subject string `json:"subject,omitempty"` Subject string `json:"subject,omitempty"`
Body string `json:"body"` Body string `json:"body"`
@@ -524,6 +538,7 @@ type userMailMessageDetailWire struct {
SenderKind string `json:"sender_kind"` SenderKind string `json:"sender_kind"`
SenderUserID *string `json:"sender_user_id,omitempty"` SenderUserID *string `json:"sender_user_id,omitempty"`
SenderUsername *string `json:"sender_username,omitempty"` SenderUsername *string `json:"sender_username,omitempty"`
SenderRaceName *string `json:"sender_race_name,omitempty"`
Subject string `json:"subject,omitempty"` Subject string `json:"subject,omitempty"`
Body string `json:"body"` Body string `json:"body"`
BodyLang string `json:"body_lang"` BodyLang string `json:"body_lang"`
@@ -540,27 +555,18 @@ type userMailMessageDetailWire struct {
Translator *string `json:"translator,omitempty"` 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 { type userMailInboxListWire struct {
Items []userMailMessageDetailWire `json:"items"` 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 { type userMailSentListWire struct {
Items []userMailSentSummaryWire `json:"items"` Items []userMailMessageDetailWire `json:"items"`
} }
type userMailUnreadCountWire struct { type userMailUnreadCountWire struct {
@@ -597,6 +603,10 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
s := *entry.SenderUsername s := *entry.SenderUsername
out.SenderUsername = &s out.SenderUsername = &s
} }
if entry.SenderRaceName != nil {
s := *entry.SenderRaceName
out.SenderRaceName = &s
}
if entry.Recipient.RecipientRaceName != nil { if entry.Recipient.RecipientRaceName != nil {
s := *entry.Recipient.RecipientRaceName s := *entry.Recipient.RecipientRaceName
out.RecipientRaceName = &s out.RecipientRaceName = &s
@@ -624,20 +634,6 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
return out 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 // mailRecipientStateToWire renders the recipient row after a
// mark-read or soft-delete call. The caller only needs the per-user // mark-read or soft-delete call. The caller only needs the per-user
// state, not the full message body again. // state, not the full message body again.
+43 -41
View File
@@ -4068,11 +4068,22 @@ components:
UserMailSendRequest: UserMailSendRequest:
type: object type: object
additionalProperties: false additionalProperties: false
required: [recipient_user_id, body] required: [body]
properties: properties:
recipient_user_id: recipient_user_id:
type: string type: string
format: uuid 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: subject:
type: string type: string
description: | description: |
@@ -4093,10 +4104,18 @@ components:
type: string type: string
format: uuid format: uuid
description: | description: |
Required when `target="user"`. Identifies the recipient One of `recipient_user_id` and `recipient_race_name` is
of the personal admin message; the recipient may be in required when `target="user"`. Identifies the recipient
any membership status (admin notifications can reach of the personal admin message by uuid; the recipient may
kicked players). 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: recipients:
type: string type: string
enum: [active, active_and_removed, all_members] enum: [active, active_and_removed, all_members]
@@ -4323,6 +4342,17 @@ components:
sender_username: sender_username:
type: string type: string
nullable: true 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: subject:
type: string type: string
body: body:
@@ -4370,41 +4400,6 @@ components:
translator: translator:
type: string type: string
description: Identifier of the translation engine that produced the cached row. 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: UserMailInboxList:
type: object type: object
additionalProperties: false additionalProperties: false
@@ -4415,6 +4410,13 @@ components:
items: items:
$ref: "#/components/schemas/UserMailMessageDetail" $ref: "#/components/schemas/UserMailMessageDetail"
UserMailSentList: 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 type: object
additionalProperties: false additionalProperties: false
required: [items] required: [items]
@@ -4422,7 +4424,7 @@ components:
items: items:
type: array type: array
items: items:
$ref: "#/components/schemas/UserMailSentSummary" $ref: "#/components/schemas/UserMailMessageDetail"
UserMailUnreadCount: UserMailUnreadCount:
type: object type: object
additionalProperties: false 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 when available, the cached translation; the client UI defaults to
the translated text and offers a "show original" toggle. 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 ### 11.5 Lifecycle hooks
Three lobby transitions land as system mail in the affected Three lobby transitions land as system mail in the affected
+14
View File
@@ -1309,6 +1309,20 @@ bulk-purge всей почты соответствующей партии.
кэш) перевод; UI по умолчанию показывает перевод и предлагает кэш) перевод; UI по умолчанию показывает перевод и предлагает
переключение «показать оригинал». переключение «показать оригинал».
Внутриигровой UI группирует личную почту по веткам по расам —
каждая личная переписка между локальным игроком и другой расой
оказывается в одной ветке, ключевая по расе собеседника.
Системные сообщения, административные уведомления и собственные
рассылки игрока (платный тариф) показываются отдельными
автономными записями в том же списке и никогда не группируются.
`read_at` и `deleted_at` поддерживают локальный счётчик
непрочитанного и кнопку удаления, но не показываются игроку —
дипломатическая почта не обещает уведомления о прочтении. Форма
compose выбирает получателя по имени расы (сервер резолвит через
`Memberships.ListMembers(game_id, "active")`); клиент не тянет
отдельный список членов. Подробнее — в
[`ui/docs/diplomail-ui.md`](../ui/docs/diplomail-ui.md).
### 11.5 Хуки жизненного цикла ### 11.5 Хуки жизненного цикла
Три транзитных перехода в лобби порождают system mail в inbox Три транзитных перехода в лобби порождают 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()) userRoutes := backendclient.UserRoutes(backend.REST())
lobbyRoutes := backendclient.LobbyRoutes(backend.REST()) lobbyRoutes := backendclient.LobbyRoutes(backend.REST())
gameRoutes := backendclient.GameRoutes(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 { for k, v := range userRoutes {
allRoutes[k] = v allRoutes[k] = v
} }
@@ -196,6 +197,9 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
for k, v := range gameRoutes { for k, v := range gameRoutes {
allRoutes[k] = v allRoutes[k] = v
} }
for k, v := range mailRoutes {
allRoutes[k] = v
}
cleanup := func() error { cleanup := func() error {
return closeRedisClient() return closeRedisClient()
@@ -63,6 +63,12 @@ func (c *RESTClient) ExecuteGameCommand(ctx context.Context, command downstream.
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err) return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err)
} }
return c.executeUserGamesReport(ctx, command.UserID, req) return c.executeUserGamesReport(ctx, command.UserID, req)
case reportmodel.MessageTypeUserGamesBattle:
req, err := transcoder.PayloadToGameBattleRequest(command.PayloadBytes)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command %q: %w", command.MessageType, err)
}
return c.executeUserGamesBattle(ctx, command.UserID, req)
default: default:
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command: unsupported message type %q", command.MessageType) return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute game command: unsupported message type %q", command.MessageType)
} }
@@ -127,6 +133,26 @@ func (c *RESTClient) executeUserGamesReport(ctx context.Context, userID string,
return projectUserGamesReportResponse(status, respBody) return projectUserGamesReportResponse(status, respBody)
} }
func (c *RESTClient) executeUserGamesBattle(ctx context.Context, userID string, req *reportmodel.GameBattleRequest) (downstream.UnaryResult, error) {
if req.GameID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.battle: game_id must not be empty")
}
if req.BattleID == uuid.Nil {
return downstream.UnaryResult{}, errors.New("execute user.games.battle: battle_id must not be empty")
}
target := fmt.Sprintf("%s/api/v1/user/games/%s/battles/%d/%s",
c.baseURL,
url.PathEscape(req.GameID.String()),
req.Turn,
url.PathEscape(req.BattleID.String()),
)
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.battle: %w", err)
}
return projectUserGamesBattleResponse(status, respBody)
}
// buildEngineCommandBody serialises a slice of typed commands into the // buildEngineCommandBody serialises a slice of typed commands into the
// JSON shape expected by backend's command/order handlers (a // JSON shape expected by backend's command/order handlers (a
// `gamerest.Command` with the actor field left empty — backend rebinds // `gamerest.Command` with the actor field left empty — backend rebinds
@@ -262,3 +288,32 @@ func projectUserGamesReportResponse(statusCode int, payload []byte) (downstream.
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode) return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
} }
} }
// projectUserGamesBattleResponse decodes the engine's BattleReport JSON
// payload (forwarded by backend's user.games.battle proxy) and
// re-encodes it as a FlatBuffers BattleReport for the signed-gRPC
// client. 404 from backend surfaces as the canonical `not_found`
// gateway error so the UI can render its "battle not found" state.
func projectUserGamesBattleResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
switch {
case statusCode == http.StatusOK:
var report reportmodel.BattleReport
if err := json.Unmarshal(payload, &report); err != nil {
return downstream.UnaryResult{}, fmt.Errorf("decode engine battle report: %w", err)
}
encoded, err := transcoder.BattleReportToPayload(&report)
if err != nil {
return downstream.UnaryResult{}, fmt.Errorf("encode battle report payload: %w", err)
}
return downstream.UnaryResult{
ResultCode: userCommandResultCodeOK,
PayloadBytes: encoded,
}, nil
case statusCode == http.StatusServiceUnavailable:
return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
case statusCode >= 400 && statusCode <= 599:
return projectUserBackendError(statusCode, payload)
default:
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
}
}
@@ -11,6 +11,7 @@ import (
"galaxy/gateway/internal/backendclient" "galaxy/gateway/internal/backendclient"
"galaxy/gateway/internal/downstream" "galaxy/gateway/internal/downstream"
ordermodel "galaxy/model/order" ordermodel "galaxy/model/order"
reportmodel "galaxy/model/report"
"galaxy/transcoder" "galaxy/transcoder"
"github.com/google/uuid" "github.com/google/uuid"
@@ -170,6 +171,78 @@ func TestExecuteUserGamesOrderGetRejectsNegativeTurn(t *testing.T) {
assert.Contains(t, err.Error(), "user.games.order.get") assert.Contains(t, err.Error(), "user.games.order.get")
} }
func TestExecuteUserGamesBattleForwardsAndDecodesResponse(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("66666666-7777-8888-9999-aaaaaaaaaaaa")
battleID := uuid.MustParse("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee")
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()+"/battles/7/"+battleID.String(),
r.URL.Path,
)
require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID))
raceID := uuid.MustParse("11111111-2222-3333-4444-555555555555")
writeJSON(t, w, http.StatusOK, map[string]any{
"id": battleID.String(),
"planet": uint(42),
"planetName": "Tau Ceti II",
"races": map[string]string{"1": raceID.String()},
"ships": map[string]map[string]any{},
"protocol": []any{},
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload, err := transcoder.GameBattleRequestToPayload(&reportmodel.GameBattleRequest{
GameID: gameID,
Turn: 7,
BattleID: battleID,
})
require.NoError(t, err)
result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, reportmodel.MessageTypeUserGamesBattle, payload))
require.NoError(t, err)
assert.Equal(t, "ok", result.ResultCode)
decoded, err := transcoder.PayloadToBattleReport(result.PayloadBytes)
require.NoError(t, err)
require.NotNil(t, decoded)
assert.Equal(t, battleID, decoded.ID)
assert.Equal(t, uint(42), decoded.Planet)
assert.Equal(t, "Tau Ceti II", decoded.PlanetName)
}
func TestExecuteUserGamesBattleMapsNotFound(t *testing.T) {
t.Parallel()
gameID := uuid.MustParse("77777777-8888-9999-aaaa-bbbbbbbbbbbb")
battleID := uuid.MustParse("99999999-aaaa-bbbb-cccc-dddddddddddd")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
writeJSON(t, w, http.StatusNotFound, map[string]any{
"error": map[string]any{
"code": "not_found",
"message": "battle not found",
},
})
}))
t.Cleanup(server.Close)
client := newRESTClient(t, server)
payload, err := transcoder.GameBattleRequestToPayload(&reportmodel.GameBattleRequest{
GameID: gameID,
Turn: 2,
BattleID: battleID,
})
require.NoError(t, err)
result, err := client.ExecuteGameCommand(context.Background(), newAuthCommand(t, reportmodel.MessageTypeUserGamesBattle, payload))
require.NoError(t, err)
assert.Equal(t, "not_found", result.ResultCode)
}
// writeJSON copy below mirrors the helper used by other test files // writeJSON copy below mirrors the helper used by other test files
// in this package; keeping it adjacent to its callers avoids // in this package; keeping it adjacent to its callers avoids
// reaching across files in a fresh test. // reaching across files in a fresh test.
@@ -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
}
+32
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"galaxy/gateway/internal/downstream" "galaxy/gateway/internal/downstream"
diplomailmodel "galaxy/model/diplomail"
lobbymodel "galaxy/model/lobby" lobbymodel "galaxy/model/lobby"
ordermodel "galaxy/model/order" ordermodel "galaxy/model/order"
reportmodel "galaxy/model/report" reportmodel "galaxy/model/report"
@@ -64,6 +65,28 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client {
ordermodel.MessageTypeUserGamesOrder: target, ordermodel.MessageTypeUserGamesOrder: target,
ordermodel.MessageTypeUserGamesOrderGet: target, ordermodel.MessageTypeUserGamesOrderGet: target,
reportmodel.MessageTypeUserGamesReport: target, reportmodel.MessageTypeUserGamesReport: target,
reportmodel.MessageTypeUserGamesBattle: target,
}
}
// 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,
} }
} }
@@ -97,9 +120,18 @@ func (c gameCommandClient) ExecuteCommand(ctx context.Context, command downstrea
return c.rest.ExecuteGameCommand(ctx, command) 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 ( var (
_ downstream.Client = unavailableClient{} _ downstream.Client = unavailableClient{}
_ downstream.Client = userCommandClient{} _ downstream.Client = userCommandClient{}
_ downstream.Client = lobbyCommandClient{} _ downstream.Client = lobbyCommandClient{}
_ downstream.Client = gameCommandClient{} _ downstream.Client = gameCommandClient{}
_ downstream.Client = mailCommandClient{}
) )
@@ -60,6 +60,7 @@ func TestRoutesCoverAllAuthenticatedMessageTypes(t *testing.T) {
ordermodel.MessageTypeUserGamesOrder, ordermodel.MessageTypeUserGamesOrder,
ordermodel.MessageTypeUserGamesOrderGet, ordermodel.MessageTypeUserGamesOrderGet,
reportmodel.MessageTypeUserGamesReport, reportmodel.MessageTypeUserGamesReport,
reportmodel.MessageTypeUserGamesBattle,
}, },
actual: backendclient.GameRoutes(nil), actual: backendclient.GameRoutes(nil),
}, },
+27
View File
@@ -101,6 +101,16 @@ const (
// the authenticated gRPC listener address. // the authenticated gRPC listener address.
authenticatedGRPCAddrEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ADDR" authenticatedGRPCAddrEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ADDR"
// authenticatedGRPCCORSAllowedOriginsEnvVar names the environment
// variable that configures the comma-separated list of browser
// origins permitted to call the authenticated Connect-Web surface.
// An empty value disables CORS entirely; the listener then refuses
// to send Access-Control-* headers and browsers block cross-origin
// fetches. Set this in any deployment that fronts the gateway
// behind a different hostname than the SvelteKit bundle (e.g.
// `https://www.galaxy.lan` calling `https://api.galaxy.lan`).
authenticatedGRPCCORSAllowedOriginsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS"
// authenticatedGRPCConnectionTimeoutEnvVar names the environment variable // authenticatedGRPCConnectionTimeoutEnvVar names the environment variable
// that configures the inbound connection handshake timeout for the // that configures the inbound connection handshake timeout for the
// authenticated gRPC listener. // authenticated gRPC listener.
@@ -542,6 +552,13 @@ type AuthenticatedGRPCConfig struct {
// AntiAbuse configures the authenticated gRPC rate limits enforced after // AntiAbuse configures the authenticated gRPC rate limits enforced after
// the request passes the transport authenticity checks. // the request passes the transport authenticity checks.
AntiAbuse AuthenticatedGRPCAntiAbuseConfig AntiAbuse AuthenticatedGRPCAntiAbuseConfig
// CORSAllowedOrigins is the exact-match list of browser origins
// permitted to call the authenticated Connect-Web surface. Empty
// disables CORS — requests without an Access-Control-Allow-Origin
// response will be blocked by the browser, which is the production
// posture when the UI and the gateway share a single hostname.
CORSAllowedOrigins []string
} }
// SessionCacheConfig describes the bounds of the gateway's in-memory // SessionCacheConfig describes the bounds of the gateway's in-memory
@@ -836,6 +853,16 @@ func LoadFromEnv() (Config, error) {
cfg.PublicHTTP.CORSAllowedOrigins = origins cfg.PublicHTTP.CORSAllowedOrigins = origins
} }
if v, ok := os.LookupEnv(authenticatedGRPCCORSAllowedOriginsEnvVar); ok {
origins := make([]string, 0)
for part := range strings.SplitSeq(v, ",") {
if trimmed := strings.TrimSpace(part); trimmed != "" {
origins = append(origins, trimmed)
}
}
cfg.AuthenticatedGRPC.CORSAllowedOrigins = origins
}
if v, ok := os.LookupEnv(backendHTTPURLEnvVar); ok { if v, ok := os.LookupEnv(backendHTTPURLEnvVar); ok {
cfg.Backend.HTTPBaseURL = v cfg.Backend.HTTPBaseURL = v
} }
+64
View File
@@ -0,0 +1,64 @@
package grpcapi
import (
"net/http"
)
// withCORS wraps next so that CORS preflight (OPTIONS) requests with an
// allow-listed Origin receive 204 plus the `Access-Control-Allow-*`
// headers Connect-Web needs, and actual requests get the matching
// `Access-Control-Allow-Origin` header echoed back. Origins are
// compared exactly: scheme, host, and port must match. An empty
// allow-list passes through untouched — the production posture when
// the UI and the gateway share one hostname.
//
// The wrapper mirrors `restapi.withCORS` but speaks plain `net/http`
// because the Connect handler is mounted on a `http.ServeMux`, not a
// gin engine. Connect-Web POSTs use `Content-Type: application/connect+json`
// which triggers a browser preflight; without these headers the
// browser surfaces "Load failed" before the Connect handler even sees
// the request.
func withCORS(allowedOrigins []string, next http.Handler) http.Handler {
allowed := make(map[string]struct{}, len(allowedOrigins))
for _, origin := range allowedOrigins {
allowed[origin] = struct{}{}
}
if len(allowed) == 0 {
return next
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin == "" {
next.ServeHTTP(w, r)
return
}
if _, ok := allowed[origin]; !ok {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Add("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
if reqHeaders := r.Header.Get("Access-Control-Request-Headers"); reqHeaders != "" {
w.Header().Set("Access-Control-Allow-Headers", reqHeaders)
} else {
// Defaults cover the Connect-Web preflight set: protocol
// version, content type, timeout, and the signed-request
// metadata the gateway interceptor expects.
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Connect-Protocol-Version, Connect-Timeout-Ms, Authorization")
}
// Expose the response headers Connect-Web needs to read on
// the client (e.g. trailers folded into headers for unary).
w.Header().Set("Access-Control-Expose-Headers", "Connect-Protocol-Version, Grpc-Status, Grpc-Message")
w.Header().Set("Access-Control-Max-Age", "3600")
w.WriteHeader(http.StatusNoContent)
return
}
// Expose the same response headers on the actual call.
w.Header().Set("Access-Control-Expose-Headers", "Connect-Protocol-Version, Grpc-Status, Grpc-Message")
next.ServeHTTP(w, r)
})
}
+4 -1
View File
@@ -169,7 +169,10 @@ func (s *Server) Run(ctx context.Context) error {
) )
mux.Handle(path, handler) mux.Handle(path, handler)
tracedHandler := otelhttp.NewHandler(mux, "authenticated_edge") // CORS runs OUTSIDE the otelhttp wrapper so preflight OPTIONS calls
// answer with 204 immediately and never enter the trace path.
corsMux := withCORS(s.cfg.CORSAllowedOrigins, mux)
tracedHandler := otelhttp.NewHandler(corsMux, "authenticated_edge")
http2Server := &http2.Server{IdleTimeout: s.cfg.ConnectionTimeout} http2Server := &http2.Server{IdleTimeout: s.cfg.ConnectionTimeout}
httpServer := &http.Server{ httpServer := &http.Server{
Handler: h2c.NewHandler(tracedHandler, http2Server), Handler: h2c.NewHandler(tracedHandler, http2Server),
+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"
)
+23
View File
@@ -9,6 +9,13 @@ import "github.com/google/uuid"
// `Report`. // `Report`.
const MessageTypeUserGamesReport = "user.games.report" const MessageTypeUserGamesReport = "user.games.report"
// MessageTypeUserGamesBattle is the authenticated gateway message type
// used to fetch one battle report through
// `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`. The
// signed payload is a FlatBuffers `GameBattleRequest`; the response is
// a FlatBuffers `BattleReport`.
const MessageTypeUserGamesBattle = "user.games.battle"
// GameReportRequest is the typed payload of MessageTypeUserGamesReport. // GameReportRequest is the typed payload of MessageTypeUserGamesReport.
// `GameID` selects the target game (the message_type alone is not // `GameID` selects the target game (the message_type alone is not
// enough; this scope is per-game) and `Turn` selects the requested // enough; this scope is per-game) and `Turn` selects the requested
@@ -20,3 +27,19 @@ type GameReportRequest struct {
// Turn is the zero-based turn number whose report is requested. // Turn is the zero-based turn number whose report is requested.
Turn uint `json:"turn"` Turn uint `json:"turn"`
} }
// GameBattleRequest is the typed payload of MessageTypeUserGamesBattle.
// `GameID` selects the target game; `Turn` is the turn the battle
// happened at (the engine partitions battles by turn for cheap lookup);
// `BattleID` is the in-game identifier returned in the report's
// battle-summary list. All three fields are required.
type GameBattleRequest struct {
// GameID identifies the game the battle belongs to.
GameID uuid.UUID `json:"game_id"`
// Turn is the turn number the battle happened at.
Turn uint `json:"turn"`
// BattleID is the engine-assigned id of the battle to fetch.
BattleID uuid.UUID `json:"battle_id"`
}
+10
View File
@@ -49,4 +49,14 @@ table BattleReport {
protocol:[BattleActionReport]; protocol:[BattleActionReport];
} }
// GameBattleRequest is the signed-gRPC request payload for
// `MessageTypeUserGamesBattle`. Gateway forwards this into the
// backend's `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`
// endpoint after resolving the caller's runtime player mapping.
table GameBattleRequest {
game_id:UUID (required);
turn:uint32;
battle_id:UUID (required);
}
root_type BattleReport; root_type BattleReport;
@@ -0,0 +1,96 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package battle
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type GameBattleRequest struct {
_tab flatbuffers.Table
}
func GetRootAsGameBattleRequest(buf []byte, offset flatbuffers.UOffsetT) *GameBattleRequest {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &GameBattleRequest{}
x.Init(buf, n+offset)
return x
}
func FinishGameBattleRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGameBattleRequest(buf []byte, offset flatbuffers.UOffsetT) *GameBattleRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &GameBattleRequest{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedGameBattleRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *GameBattleRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *GameBattleRequest) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *GameBattleRequest) GameId(obj *UUID) *UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func (rcv *GameBattleRequest) Turn() uint32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetUint32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *GameBattleRequest) MutateTurn(n uint32) bool {
return rcv._tab.MutateUint32Slot(6, n)
}
func (rcv *GameBattleRequest) BattleId(obj *UUID) *UUID {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
x := o + rcv._tab.Pos
if obj == nil {
obj = new(UUID)
}
obj.Init(rcv._tab.Bytes, x)
return obj
}
return nil
}
func GameBattleRequestStart(builder *flatbuffers.Builder) {
builder.StartObject(3)
}
func GameBattleRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
}
func GameBattleRequestAddTurn(builder *flatbuffers.Builder, turn uint32) {
builder.PrependUint32Slot(1, turn, 0)
}
func GameBattleRequestAddBattleId(builder *flatbuffers.Builder, battleId flatbuffers.UOffsetT) {
builder.PrependStructSlot(2, flatbuffers.UOffsetT(battleId), 0)
}
func GameBattleRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+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()
}
+52
View File
@@ -382,3 +382,55 @@ func encodeBattleOffsetVector(
} }
return builder.EndVector(length) return builder.EndVector(length)
} }
// GameBattleRequestToPayload converts model.GameBattleRequest to
// FlatBuffers bytes suitable for the authenticated gateway transport.
func GameBattleRequestToPayload(req *model.GameBattleRequest) ([]byte, error) {
if req == nil {
return nil, errors.New("encode game battle request payload: request is nil")
}
builder := flatbuffers.NewBuilder(64)
gameHi, gameLo := uuidToHiLo(req.GameID)
battleHi, battleLo := uuidToHiLo(req.BattleID)
fbs.GameBattleRequestStart(builder)
fbs.GameBattleRequestAddGameId(builder, fbs.CreateUUID(builder, gameHi, gameLo))
fbs.GameBattleRequestAddTurn(builder, uint32(req.Turn))
fbs.GameBattleRequestAddBattleId(builder, fbs.CreateUUID(builder, battleHi, battleLo))
offset := fbs.GameBattleRequestEnd(builder)
fbs.FinishGameBattleRequestBuffer(builder, offset)
return builder.FinishedBytes(), nil
}
// PayloadToGameBattleRequest converts FlatBuffers payload bytes into
// model.GameBattleRequest.
func PayloadToGameBattleRequest(data []byte) (result *model.GameBattleRequest, err error) {
if len(data) == 0 {
return nil, errors.New("decode game battle request payload: data is empty")
}
defer func() {
if recovered := recover(); recovered != nil {
result = nil
err = fmt.Errorf("decode game battle request payload: panic recovered: %v", recovered)
}
}()
req := fbs.GetRootAsGameBattleRequest(data, 0)
gameID := req.GameId(nil)
if gameID == nil {
return nil, errors.New("decode game battle request payload: game_id is missing")
}
battleID := req.BattleId(nil)
if battleID == nil {
return nil, errors.New("decode game battle request payload: battle_id is missing")
}
return &model.GameBattleRequest{
GameID: uuidFromHiLo(gameID.Hi(), gameID.Lo()),
Turn: uint(req.Turn()),
BattleID: uuidFromHiLo(battleID.Hi(), battleID.Lo()),
}, nil
}
+3 -1
View File
@@ -14,7 +14,9 @@ BACKEND_DEV_SANDBOX_ENGINE_VERSION=0.1.0
BACKEND_DEV_SANDBOX_PLAYER_COUNT=20 BACKEND_DEV_SANDBOX_PLAYER_COUNT=20
# `123456` short-circuits the email-code path for the dev account. # `123456` short-circuits the email-code path for the dev account.
# Leave empty in environments where real Mailpit codes must be used. # This is also the docker-compose default — set the variable to an
# empty string here when the environment must rely on real Mailpit
# codes (e.g. mail-flow QA).
BACKEND_AUTH_DEV_FIXED_CODE=123456 BACKEND_AUTH_DEV_FIXED_CODE=123456
# Name of the external Docker bridge the host Caddy is attached to. # Name of the external Docker bridge the host Caddy is attached to.
+9
View File
@@ -20,6 +20,15 @@
@api host api.galaxy.lan @api host api.galaxy.lan
handle @api { handle @api {
# Connect-Web (authenticated) lives on a separate listener # Connect-Web (authenticated) lives on a separate listener
# (`GATEWAY_AUTHENTICATED_GRPC_ADDR=:9090`). Anything else —
# public auth, healthz — is the public REST listener on
# `:8080`. The split mirrors the Vite dev-server proxy in
# `ui/frontend/vite.config.ts`.
@connect path /galaxy.gateway.v1.EdgeGateway/*
handle @connect {
reverse_proxy galaxy-api:9090
}
reverse_proxy galaxy-api:8080
} }
} }
} }
+133
View File
@@ -0,0 +1,133 @@
# `tools/dev-deploy/` — known issues
Issues that surface in the long-lived dev environment but are not yet
fixed. Each entry lists the observed symptom, the diagnostic evidence,
the working hypothesis, and the open questions that have to be
answered before a fix lands.
## Dev Sandbox game flips to `cancelled` after a `dev-deploy` redispatch
### Symptom
A previously `running` "Dev Sandbox" game (created by
`backend/internal/devsandbox`) transitions to `cancelled` ~15 minutes
after a `dev-deploy.yaml` workflow_dispatch run finishes. The user's
browser session survives (the same `device_session_id` keeps working),
but the lobby shows no game because the only game it had is now
terminal. `purgeTerminalSandboxGames` does pick it up on the **next**
boot and creates a fresh sandbox — but the first redispatch leaves
the user with an empty lobby until backend restarts again.
### Diagnostic evidence
Backend logs from the broken cycle (timestamps abbreviated):
```text
20:24:40 dev_sandbox: purged terminal sandbox game game_id=<prev> status=cancelled
20:24:40 dev_sandbox: memberships ensured count=20 game_id=<new>
20:24:40 dev_sandbox: bootstrap complete user_id=<owner> game_id=<new> status=starting
...
20:25:09 user mail sent failed (diplomail tables missing — unrelated)
...
20:39:40 lobby: game cancelled by runtime reconciler game_id=<new>
op=reconcile status=removed message="container disappeared"
```
Between 20:24:40 (`status=starting`) and 20:39:40 (reconciler cancel)
the backend logs are silent on the runtime / engine paths — no
`engine spawned`, no `engine container started`, no `runtime
transition` lines. The reconciler then fires and reports the engine
container as missing.
`docker ps -a --filter 'label=org.opencontainers.image.title=galaxy-game-engine'`
returns no rows during this window — the engine container is neither
running nor stopped on the host, so it either was never spawned or
was removed before the host snapshot.
### What has been ruled out
A live `docker inspect` on a healthy engine container shows:
```text
Labels: galaxy.backend=1, galaxy.engine_version=0.1.0,
galaxy.game_id=<uuid>,
org.opencontainers.image.title=galaxy-game-engine,
com.galaxy.{cpu_quota,memory,pids_limit}
AutoRemove: false
RestartPolicy: on-failure
NetworkMode: galaxy-dev-internal
```
There are no `com.docker.compose.*` labels and `AutoRemove=false`,
so `--remove-orphans` cannot reap the engine and a `--rm`-style
self-destruct is not in play. Two redispatches captured under
`docker events --filter event=create,start,die,destroy,kill,stop`
also confirmed it: across both runs the only `die` / `destroy`
events were for `galaxy-dev-{backend,api,caddy}`. The live engine
container survived both redispatches, and the reconciler that
fires 60 seconds after the new backend boots correctly matched
it through `byGameID` / `byContainerID`.
`backend/internal/runtime/service.go` only removes engine
containers from the explicit `runStop` / `runRestart` / `runPatch`
paths. There is no `runtime.Service.Shutdown` that proactively
kills containers on backend exit, so a graceful SIGTERM to
`galaxy-dev-backend` will not touch its child engine containers.
### Host-side hypotheses considered and rejected by the owner
The natural follow-up suspects after compose was cleared — host-side
`docker prune` cron jobs, a manual `docker rm`, an out-of-band
`dockerd` restart, and an idle-state engine crash — were all
rejected by the project owner: the dev host runs none of those
periodic cleanups, no one manually removed the container, dockerd
was not restarted in the window, and the engine binary does not
crash while idling on API calls.
### Best remaining suspicion
Something the `dev-deploy.yaml` CI run does between successful
image builds and the final `docker compose up -d --wait
--remove-orphans` clobbers the previously-spawned engine container.
The chain at runtime contains:
1. `docker build -t galaxy-engine:dev -f game/Dockerfile .`
2. `docker compose build galaxy-backend galaxy-api`
3. `docker run --rm` alpine for the UI volume seed
4. `docker compose up -d --wait --remove-orphans`
None of these *should* touch an unmanaged engine container, but
the reproduction window points squarely inside this sequence. A
deliberate next reproduction with `docker events --since 0` armed
*before* the deploy starts and live for the entire job — captured
end-to-end on the dev host, not just the chunk after backend
recreate — would pin which step emits the `destroy` on the engine.
### Status
Parked. The bug is mildly disruptive (one redispatch + a manual
`make seed-ui`-style follow-up brings the sandbox back) and the
remaining hypotheses are speculative. If the symptom recurs, attach
the next bad-window `docker events` capture to this entry and
reopen. A `tools/dev-deploy/` rewrite may obviate the issue
entirely; that is on the project owner's medium-term list.
### Workaround in use today
When the sandbox game flips to `cancelled`, redispatch `dev-deploy`:
```sh
curl -X POST -n -H 'Content-Type: application/json' \
-d '{"ref":"<branch>"}' \
https://gitea.iliadenisov.ru/api/v1/repos/developer/galaxy-game/actions/workflows/dev-deploy.yaml/dispatches
```
The next boot's `purgeTerminalSandboxGames` removes the cancelled
row, `findOrCreateSandboxGame` creates a fresh one, and
`ensureMembershipsAndDrive` puts the new game back to `running`.
### Owner
Unassigned. File an issue once we have the runtime / reconciler
analysis above; reference this section in the issue body so future
redeploys can short-circuit the diagnostic loop.
+1
View File
@@ -62,6 +62,7 @@ seed-ui:
@echo "building UI (vite build)…" @echo "building UI (vite build)…"
(cd $(REPO_ROOT)/ui/frontend && \ (cd $(REPO_ROOT)/ui/frontend && \
VITE_GATEWAY_BASE_URL=https://api.galaxy.lan \ VITE_GATEWAY_BASE_URL=https://api.galaxy.lan \
VITE_GALAXY_DEV_AFFORDANCES=true \
VITE_GATEWAY_RESPONSE_PUBLIC_KEY=$$(cat $(REPO_ROOT)/ui/frontend/.env.development \ VITE_GATEWAY_RESPONSE_PUBLIC_KEY=$$(cat $(REPO_ROOT)/ui/frontend/.env.development \
| sed -n 's/^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=//p') \ | sed -n 's/^VITE_GATEWAY_RESPONSE_PUBLIC_KEY=//p') \
pnpm build) pnpm build)
+13 -5
View File
@@ -91,14 +91,16 @@ calls `make clean-data`.
## Logging in ## Logging in
The same dev-mode email-code override as `tools/local-dev/` applies: The same dev-mode email-code override as `tools/local-dev/` applies,
and the dev-deploy compose ships with it enabled by default:
1. Enter `dev@galaxy.lan` (or whatever `BACKEND_DEV_SANDBOX_EMAIL` 1. Enter `dev@galaxy.lan` (or whatever `BACKEND_DEV_SANDBOX_EMAIL`
resolves to) in the login form. resolves to) in the login form.
2. Submit `123456` as the code if `BACKEND_AUTH_DEV_FIXED_CODE` is 2. Submit `123456` as the code — the docker-compose default for
non-empty. Otherwise open Mailpit at `BACKEND_AUTH_DEV_FIXED_CODE` is `123456`, so the bcrypt-hashed
`http://galaxy-mailpit:8025/` from inside the network or proxy it email code stays a fallback. To force real Mailpit codes (e.g. for
through the host Caddy when needed. mail-flow QA), set `BACKEND_AUTH_DEV_FIXED_CODE=` (empty) in a
local `.env` and `make rebuild`.
The fixed-code override is rejected by production env loaders, so it The fixed-code override is rejected by production env loaders, so it
cannot leak into the prod environment. cannot leak into the prod environment.
@@ -175,6 +177,12 @@ make clean-data Stop everything and wipe volumes + game-state dir
- `.env.example` — non-secret defaults for the compose `${VAR:-}` - `.env.example` — non-secret defaults for the compose `${VAR:-}`
expansions. Copy to `.env` if you want host-local overrides. expansions. Copy to `.env` if you want host-local overrides.
## Known issues
See [`KNOWN-ISSUES.md`](KNOWN-ISSUES.md) for symptoms that surface
in the long-lived dev environment but are not yet fixed (currently:
the sandbox game flipping to `cancelled` after a redispatch).
## Relationship to other infrastructure ## Relationship to other infrastructure
- `tools/local-dev/` — single-developer playground, host-port mapped, - `tools/local-dev/` — single-developer playground, host-port mapped,
+13 -2
View File
@@ -101,8 +101,18 @@ services:
BACKEND_NOTIFICATION_WORKER_INTERVAL: 500ms BACKEND_NOTIFICATION_WORKER_INTERVAL: 500ms
BACKEND_OTEL_TRACES_EXPORTER: none BACKEND_OTEL_TRACES_EXPORTER: none
BACKEND_OTEL_METRICS_EXPORTER: none BACKEND_OTEL_METRICS_EXPORTER: none
BACKEND_AUTH_DEV_FIXED_CODE: ${BACKEND_AUTH_DEV_FIXED_CODE:-} # Long-lived dev environment always opts into the fixed-code
BACKEND_DEV_SANDBOX_EMAIL: ${BACKEND_DEV_SANDBOX_EMAIL:-} # override so a returning developer can sign in with `123456`
# even after the matching browser session was cleared (the real
# bcrypt-hashed code is single-use). Set the var to an empty
# string in `.env` to disable.
BACKEND_AUTH_DEV_FIXED_CODE: ${BACKEND_AUTH_DEV_FIXED_CODE:-123456}
# Long-lived dev environment always bootstraps the "Dev Sandbox"
# game owned by this email so a freshly redeployed stack already
# has one ready-to-play game in the lobby. Set the variable to an
# empty string in `.env` to disable the bootstrap (e.g. for a
# cold-start QA pass).
BACKEND_DEV_SANDBOX_EMAIL: ${BACKEND_DEV_SANDBOX_EMAIL:-dev@galaxy.lan}
BACKEND_DEV_SANDBOX_ENGINE_IMAGE: ${BACKEND_DEV_SANDBOX_ENGINE_IMAGE:-galaxy-engine:dev} BACKEND_DEV_SANDBOX_ENGINE_IMAGE: ${BACKEND_DEV_SANDBOX_ENGINE_IMAGE:-galaxy-engine:dev}
BACKEND_DEV_SANDBOX_ENGINE_VERSION: ${BACKEND_DEV_SANDBOX_ENGINE_VERSION:-0.1.0} BACKEND_DEV_SANDBOX_ENGINE_VERSION: ${BACKEND_DEV_SANDBOX_ENGINE_VERSION:-0.1.0}
BACKEND_DEV_SANDBOX_PLAYER_COUNT: ${BACKEND_DEV_SANDBOX_PLAYER_COUNT:-20} BACKEND_DEV_SANDBOX_PLAYER_COUNT: ${BACKEND_DEV_SANDBOX_PLAYER_COUNT:-20}
@@ -161,6 +171,7 @@ services:
# https://api.galaxy.lan. Browsers therefore issue cross-origin # https://api.galaxy.lan. Browsers therefore issue cross-origin
# requests to the gateway and need an explicit allow-list. # requests to the gateway and need an explicit allow-list.
GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan" GATEWAY_PUBLIC_HTTP_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan"
GATEWAY_AUTHENTICATED_GRPC_CORS_ALLOWED_ORIGINS: "https://www.galaxy.lan"
# Anti-abuse defaults are looser than production: the dev # Anti-abuse defaults are looser than production: the dev
# environment is shared by a handful of trusted testers who # environment is shared by a handful of trusted testers who
# frequently hammer the same identity to reproduce flows. # frequently hammer the same identity to reproduce flows.
+1 -1
View File
@@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm
WASM_EXEC := frontend/static/wasm_exec.js WASM_EXEC := frontend/static/wasm_exec.js
TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null) TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null)
FBS_OUT := frontend/src/proto/galaxy/fbs 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 ../pkg/schema/fbs/battle.fbs
help: help:
@echo "ui targets:" @echo "ui targets:"
+66 -2
View File
@@ -3070,9 +3070,73 @@ bottom):
- animated transitions when survivors re-distribute after an - animated transitions when survivors re-distribute after an
elimination (currently hard-jumps). 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 Goal: implement a mail inbox and compose flow as a dedicated view that
replaces the map. 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.
+6
View File
@@ -16,3 +16,9 @@ VITE_GATEWAY_BASE_URL=http://localhost:5173
# key. Pairs with `tools/local-dev/keys/gateway-response.pem`. The pair # key. Pairs with `tools/local-dev/keys/gateway-response.pem`. The pair
# is dev-only — see `tools/local-dev/keys/README.md` before rotating. # is dev-only — see `tools/local-dev/keys/README.md` before rotating.
VITE_GATEWAY_RESPONSE_PUBLIC_KEY=nIG54tCuNiIKrazt8Hh7YxmmU/BhpseGhIIgj164Chw= VITE_GATEWAY_RESPONSE_PUBLIC_KEY=nIG54tCuNiIKrazt8Hh7YxmmU/BhpseGhIIgj164Chw=
# Opt in to dev-time UI affordances that should never reach a
# production bundle — currently the synthetic-report loader in the
# lobby. Mirror this flag in any long-lived dev build (e.g.
# `dev-deploy.yaml`); the prod build path leaves it unset.
VITE_GALAXY_DEV_AFFORDANCES=true
+198 -25
View File
@@ -1,21 +1,33 @@
// Battle-report fetcher used by the Battle Viewer page. // Battle-report fetcher used by the Battle Viewer page.
// //
// Phase 27 ships the BattleViewer as a logically isolated component // Phase 28 migrates this surface off the raw REST passthrough onto the
// that accepts a `BattleReport` matching `pkg/model/report/battle.go`. // `user.games.battle` ConnectRPC command — the same signed envelope the
// This module owns the type mirror and a single `fetchBattle` entry // other authenticated traffic rides. The synthetic-mode short-circuit
// point. In synthetic mode (development & e2e fixtures), the loader // stays so DEV / e2e tests can render fixtures without a live gateway.
// falls back to a local fixture so the UI tests don't depend on a
// running engine; otherwise it issues a real `GET` against the
// backend gateway route added in Phase 27 step 3.
import { Builder, ByteBuffer } from "flatbuffers";
import type { GalaxyClient } from "./galaxy-client";
import { uuidToHiLo } from "./game-state";
import { isSyntheticGameId } from "./synthetic-report"; import { isSyntheticGameId } from "./synthetic-report";
import { lookupSyntheticBattle } from "./synthetic-battle"; import { lookupSyntheticBattle } from "./synthetic-battle";
import {
BattleActionReport as FbsBattleActionReport,
BattleReport as FbsBattleReport,
BattleReportGroup as FbsBattleReportGroup,
GameBattleRequest,
RaceEntry,
ShipEntry,
UUID,
} from "../proto/galaxy/fbs/battle";
import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby";
/** /**
* BattleReport is the wire shape returned by the engine endpoint * BattleReport mirrors the on-wire battle shape the BattleViewer
* `GET /api/v1/battle/:turn/:uuid` and forwarded by the backend * renders. Fields match `pkg/model/report/battle.go`; integer-keyed
* gateway as `GET /api/v1/user/games/{game_id}/battles/{turn}/{battle_id}`. * maps from the underlying model are surfaced as string-keyed
* Fields mirror `pkg/model/report/battle.go`. * `Record`s so the existing components (race / ship lookup, mass
* scaling, timeline) keep their current types.
*/ */
export interface BattleReport { export interface BattleReport {
id: string; id: string;
@@ -46,20 +58,28 @@ export interface BattleActionReport {
} }
export class BattleFetchError extends Error { export class BattleFetchError extends Error {
constructor(public readonly status: number, message: string) { constructor(
public readonly status: number,
message: string,
) {
super(message); super(message);
this.name = "BattleFetchError"; this.name = "BattleFetchError";
} }
} }
const MESSAGE_TYPE = "user.games.battle";
const RESULT_CODE_OK = "ok";
/** /**
* fetchBattle returns the `BattleReport` for the supplied game, turn, * fetchBattle returns the `BattleReport` for the supplied game, turn,
* and battle id. In synthetic-report mode (DEV / e2e) the lookup is * and battle id. In synthetic-report mode (DEV / e2e) the lookup is
* served from `synthetic-battle.ts`; otherwise the function calls the * served from `synthetic-battle.ts`; otherwise the function calls the
* backend gateway route. Throws `BattleFetchError` with the upstream * `user.games.battle` ConnectRPC command through the supplied
* status on validation or transport failure. * `GalaxyClient`. Throws `BattleFetchError` with the upstream HTTP
* status (or `0` for transport-level failures) on error.
*/ */
export async function fetchBattle( export async function fetchBattle(
client: GalaxyClient,
gameId: string, gameId: string,
turn: number, turn: number,
battleId: string, battleId: string,
@@ -71,18 +91,171 @@ export async function fetchBattle(
} }
return fixture; return fixture;
} }
const path = `/api/v1/user/games/${encodeURIComponent(gameId)}/battles/${turn}/${encodeURIComponent(battleId)}`;
const response = await fetch(path, { const payload = encodeRequest(gameId, turn, battleId);
headers: { Accept: "application/json" }, const result = await client.executeCommand(MESSAGE_TYPE, payload);
}); if (result.resultCode !== RESULT_CODE_OK) {
if (response.status === 404) { throw decodeError(result.resultCode, result.payloadBytes);
throw new BattleFetchError(404, "battle not found");
} }
if (!response.ok) { return decodeBattleReport(result.payloadBytes);
throw new BattleFetchError( }
response.status,
`battle fetch failed: ${response.status}`, function encodeRequest(
gameId: string,
turn: number,
battleId: string,
): Uint8Array {
const builder = new Builder(96);
const [gameHi, gameLo] = uuidToHiLo(gameId);
const [battleHi, battleLo] = uuidToHiLo(battleId);
GameBattleRequest.startGameBattleRequest(builder);
GameBattleRequest.addGameId(
builder,
UUID.createUUID(builder, gameHi, gameLo),
);
GameBattleRequest.addTurn(builder, turn);
GameBattleRequest.addBattleId(
builder,
UUID.createUUID(builder, battleHi, battleLo),
);
builder.finish(GameBattleRequest.endGameBattleRequest(builder));
return builder.asUint8Array();
}
function decodeError(resultCode: string, payload: Uint8Array): BattleFetchError {
let message = resultCode;
try {
const errorResponse = FbsErrorResponse.getRootAsErrorResponse(
new ByteBuffer(payload),
);
const body = errorResponse.error();
if (body) {
message = body.message() ?? resultCode;
}
} catch (_err) {
// fall through to the raw result code
}
const status = mapResultCodeToStatus(resultCode);
return new BattleFetchError(status, message);
}
function mapResultCodeToStatus(resultCode: string): number {
switch (resultCode) {
case "not_found":
return 404;
case "invalid_request":
return 400;
case "forbidden":
return 403;
case "conflict":
return 409;
case "service_unavailable":
return 503;
default:
return 500;
}
}
function decodeBattleReport(bytes: Uint8Array): BattleReport {
const fb = FbsBattleReport.getRootAsBattleReport(new ByteBuffer(bytes));
const id = uuidStringFromFB(fb.id());
if (id === null) {
throw new BattleFetchError(500, "battle response missing id");
}
return {
id,
planet: Number(fb.planet()),
planetName: fb.planetName() ?? "",
races: decodeRaces(fb),
ships: decodeShips(fb),
protocol: decodeProtocol(fb),
};
}
function decodeRaces(fb: FbsBattleReport): Record<string, string> {
const out: Record<string, string> = {};
const total = fb.racesLength();
const item = new RaceEntry();
for (let i = 0; i < total; i++) {
if (!fb.races(i, item)) continue;
const valueUUID = item.value();
const value = uuidStringFromFB(valueUUID);
if (value === null) continue;
out[item.key().toString()] = value;
}
return out;
}
function decodeShips(fb: FbsBattleReport): Record<string, BattleReportGroup> {
const out: Record<string, BattleReportGroup> = {};
const total = fb.shipsLength();
const entry = new ShipEntry();
for (let i = 0; i < total; i++) {
if (!fb.ships(i, entry)) continue;
const group = entry.value();
if (group === null) continue;
out[entry.key().toString()] = decodeGroup(group);
}
return out;
}
function decodeGroup(group: FbsBattleReportGroup): BattleReportGroup {
const tech: Record<string, number> = {};
const techLen = group.techLength();
for (let i = 0; i < techLen; i++) {
const t = group.tech(i);
if (!t) continue;
const key = t.key();
if (key === null) continue;
tech[key] = t.value();
}
return {
race: (group.race() ?? "") as string,
className: (group.className() ?? "") as string,
tech,
num: Number(group.number()),
numLeft: Number(group.numberLeft()),
loadType: (group.loadType() ?? "") as string,
loadQuantity: group.loadQuantity(),
inBattle: group.inBattle(),
};
}
function decodeProtocol(fb: FbsBattleReport): BattleActionReport[] {
const out: BattleActionReport[] = [];
const total = fb.protocolLength();
const item = new FbsBattleActionReport();
for (let i = 0; i < total; i++) {
if (!fb.protocol(i, item)) continue;
out.push({
a: Number(item.attacker()),
sa: Number(item.attackerShipClass()),
d: Number(item.defender()),
sd: Number(item.defenderShipClass()),
x: item.destroyed(),
});
}
return out;
}
function uuidStringFromFB(uuid: UUID | null): string | null {
if (uuid === null) return null;
const hi = uuid.hi();
const lo = uuid.lo();
const hex = bigUintTo16Hex(hi) + bigUintTo16Hex(lo);
return (
hex.slice(0, 8) +
"-" +
hex.slice(8, 12) +
"-" +
hex.slice(12, 16) +
"-" +
hex.slice(16, 20) +
"-" +
hex.slice(20, 32)
); );
} }
return (await response.json()) as BattleReport;
function bigUintTo16Hex(value: bigint): string {
return value.toString(16).padStart(16, "0");
} }
+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));
}
+16 -1
View File
@@ -25,6 +25,10 @@ viewer keeps its prop-driven contract.
RENDERED_REPORT_CONTEXT_KEY, RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource, type RenderedReportSource,
} from "$lib/rendered-report.svelte"; } from "$lib/rendered-report.svelte";
import {
GALAXY_CLIENT_CONTEXT_KEY,
type GalaxyClientHandle,
} from "$lib/galaxy-client-context.svelte";
import { import {
MapShipClassLookup, MapShipClassLookup,
type ShipClassLookup, type ShipClassLookup,
@@ -46,6 +50,9 @@ viewer keeps its prop-driven contract.
const rendered = getContext<RenderedReportSource | undefined>( const rendered = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY, RENDERED_REPORT_CONTEXT_KEY,
); );
const galaxyClient = getContext<GalaxyClientHandle | undefined>(
GALAXY_CLIENT_CONTEXT_KEY,
);
const shipClassLookup = $derived.by<ShipClassLookup>(() => { const shipClassLookup = $derived.by<ShipClassLookup>(() => {
const map = new Map<string, ShipClassRef>(); const map = new Map<string, ShipClassRef>();
@@ -85,8 +92,16 @@ viewer keeps its prop-driven contract.
state = { kind: "not_found" }; state = { kind: "not_found" };
return; return;
} }
const client = galaxyClient?.client ?? null;
if (!client) {
// Layout populates the client after the boot Promise.all
// resolves; stay in `loading` so the effect re-runs once
// the handle becomes non-null.
state = { kind: "loading" }; state = { kind: "loading" };
fetchBattle(gameId, turn, battleId) return;
}
state = { kind: "loading" };
fetchBattle(client, gameId, turn, battleId)
.then((report) => { .then((report) => {
state = { kind: "ready", report }; state = { kind: "ready", report };
}) })
+191 -11
View File
@@ -1,27 +1,207 @@
<!-- <!--
Phase 10 stub for the diplomatic-mail active view. Phase 28 wires the Phase 28 active-view for the diplomatic mail. Replaces the Phase 10
real mail listing. 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"> <script lang="ts">
import { page } from "$app/state";
import { i18n } from "$lib/i18n/index.svelte"; 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> </script>
<section class="active-view" data-testid="active-view-mail"> <section class="mail" data-testid="active-view-mail">
<header class="mail-header">
<h2>{i18n.t("game.view.mail")}</h2> <h2>{i18n.t("game.view.mail")}</h2>
<p>{i18n.t("game.shell.coming_soon")}</p> <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> </section>
<style> <style>
.active-view { .mail {
padding: 1.5rem; display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
font-family: system-ui, sans-serif; font-family: system-ui, sans-serif;
} }
.active-view h2 { .mail-header {
margin: 0 0 0.5rem; display: flex;
align-items: center;
justify-content: space-between;
}
.mail-header h2 {
margin: 0;
font-size: 1.1rem; font-size: 1.1rem;
} }
.active-view p { .compose-btn {
margin: 0; font: inherit;
color: #555; 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> </style>
@@ -0,0 +1,239 @@
<!--
Phase 28 — compose dialog for diplomatic mail. The recipient picker
reads `gameState.report.races[]` (Phase 22); the kind toggle exposes
personal / broadcast. The admin compose path lives in the
server-side admin tooling, not in the in-game UI, so it is not
surfaced here. Broadcast 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";
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 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;
}
if (kind === "personal" && 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;
}
await mailStore.composeBroadcast({ 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>
</select>
</label>
{#if kind === "personal"}
<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 { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
import { mailStore } from "$lib/mail-store.svelte";
type Props = { gameId: string }; type Props = { gameId: string };
let { gameId }: Props = $props(); let { gameId }: Props = $props();
const mailUnread = $derived(mailStore.unreadCount);
let open = $state(false); let open = $state(false);
let rootEl: HTMLDivElement | null = $state(null); let rootEl: HTMLDivElement | null = $state(null);
@@ -122,9 +125,15 @@ polishes microcopy.
type="button" type="button"
role="menuitem" role="menuitem"
data-testid="view-menu-item-mail" data-testid="view-menu-item-mail"
class="with-badge"
onclick={() => go(`/games/${gameId}/mail`)} 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>
<button <button
type="button" type="button"
@@ -200,6 +209,21 @@ polishes microcopy.
border: 0; border: 0;
cursor: pointer; 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 > button:hover,
.surface > details > summary:hover { .surface > details > summary:hover {
background: #1c2238; background: #1c2238;
+40
View File
@@ -123,6 +123,46 @@ const en = {
"game.view.report": "turn report", "game.view.report": "turn report",
"game.view.battle": "battle log", "game.view.battle": "battle log",
"game.view.mail": "diplomatic mail", "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.ship_class": "ship-class designer",
"game.view.designer.science": "science designer", "game.view.designer.science": "science designer",
"game.sidebar.tab.calculator": "calculator", "game.sidebar.tab.calculator": "calculator",
+40
View File
@@ -124,6 +124,46 @@ const ru: Record<keyof typeof en, string> = {
"game.view.report": "отчёт хода", "game.view.report": "отчёт хода",
"game.view.battle": "журнал боёв", "game.view.battle": "журнал боёв",
"game.view.mail": "дипломатическая почта", "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.ship_class": "конструктор класса кораблей",
"game.view.designer.science": "редактор наук", "game.view.designer.science": "редактор наук",
"game.sidebar.tab.calculator": "калькулятор", "game.sidebar.tab.calculator": "калькулятор",
+388
View File
@@ -0,0 +1,388 @@
// 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;
}
// Broadcast and admin fan-outs return one row per recipient from
// the `/sent` endpoint (so the admin UI sees the materialised
// audience). The in-game list pane collapses them by `message_id`
// — without this dedupe the {#each} key in `thread-list.svelte`
// repeats and Svelte 5 aborts the render with `each_key_duplicate`.
const seen = new Set<string>();
const dedupedStandalones: MailStandalone[] = [];
for (const s of standalones) {
if (seen.has(s.message.messageId)) {
continue;
}
seen.add(s.message.messageId);
dedupedStandalones.push(s);
}
const entries: MailListEntry[] = [
...Array.from(threadsByRace.values()),
...dedupedStandalones,
];
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,12 @@
// 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 { BattleActionReport, BattleActionReportT } from './battle/battle-action-report.js';
export { BattleReport, BattleReportT } from './battle/battle-report.js';
export { BattleReportGroup, BattleReportGroupT } from './battle/battle-report-group.js';
export { GameBattleRequest, GameBattleRequestT } from './battle/game-battle-request.js';
export { RaceEntry, RaceEntryT } from './battle/race-entry.js';
export { ShipEntry, ShipEntryT } from './battle/ship-entry.js';
export { TechEntry, TechEntryT } from './battle/tech-entry.js';
export { UUID, UUIDT } from './battle/uuid.js';
@@ -0,0 +1,130 @@
// 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 BattleActionReport implements flatbuffers.IUnpackableObject<BattleActionReportT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):BattleActionReport {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsBattleActionReport(bb:flatbuffers.ByteBuffer, obj?:BattleActionReport):BattleActionReport {
return (obj || new BattleActionReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsBattleActionReport(bb:flatbuffers.ByteBuffer, obj?:BattleActionReport):BattleActionReport {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new BattleActionReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
attacker():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
attackerShipClass():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
defender():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
defenderShipClass():bigint {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
destroyed():boolean {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
static startBattleActionReport(builder:flatbuffers.Builder) {
builder.startObject(5);
}
static addAttacker(builder:flatbuffers.Builder, attacker:bigint) {
builder.addFieldInt64(0, attacker, BigInt('0'));
}
static addAttackerShipClass(builder:flatbuffers.Builder, attackerShipClass:bigint) {
builder.addFieldInt64(1, attackerShipClass, BigInt('0'));
}
static addDefender(builder:flatbuffers.Builder, defender:bigint) {
builder.addFieldInt64(2, defender, BigInt('0'));
}
static addDefenderShipClass(builder:flatbuffers.Builder, defenderShipClass:bigint) {
builder.addFieldInt64(3, defenderShipClass, BigInt('0'));
}
static addDestroyed(builder:flatbuffers.Builder, destroyed:boolean) {
builder.addFieldInt8(4, +destroyed, +false);
}
static endBattleActionReport(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createBattleActionReport(builder:flatbuffers.Builder, attacker:bigint, attackerShipClass:bigint, defender:bigint, defenderShipClass:bigint, destroyed:boolean):flatbuffers.Offset {
BattleActionReport.startBattleActionReport(builder);
BattleActionReport.addAttacker(builder, attacker);
BattleActionReport.addAttackerShipClass(builder, attackerShipClass);
BattleActionReport.addDefender(builder, defender);
BattleActionReport.addDefenderShipClass(builder, defenderShipClass);
BattleActionReport.addDestroyed(builder, destroyed);
return BattleActionReport.endBattleActionReport(builder);
}
unpack(): BattleActionReportT {
return new BattleActionReportT(
this.attacker(),
this.attackerShipClass(),
this.defender(),
this.defenderShipClass(),
this.destroyed()
);
}
unpackTo(_o: BattleActionReportT): void {
_o.attacker = this.attacker();
_o.attackerShipClass = this.attackerShipClass();
_o.defender = this.defender();
_o.defenderShipClass = this.defenderShipClass();
_o.destroyed = this.destroyed();
}
}
export class BattleActionReportT implements flatbuffers.IGeneratedObject {
constructor(
public attacker: bigint = BigInt('0'),
public attackerShipClass: bigint = BigInt('0'),
public defender: bigint = BigInt('0'),
public defenderShipClass: bigint = BigInt('0'),
public destroyed: boolean = false
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return BattleActionReport.createBattleActionReport(builder,
this.attacker,
this.attackerShipClass,
this.defender,
this.defenderShipClass,
this.destroyed
);
}
}
@@ -0,0 +1,201 @@
// 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 { TechEntry, TechEntryT } from '../battle/tech-entry.js';
export class BattleReportGroup implements flatbuffers.IUnpackableObject<BattleReportGroupT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):BattleReportGroup {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsBattleReportGroup(bb:flatbuffers.ByteBuffer, obj?:BattleReportGroup):BattleReportGroup {
return (obj || new BattleReportGroup()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsBattleReportGroup(bb:flatbuffers.ByteBuffer, obj?:BattleReportGroup):BattleReportGroup {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new BattleReportGroup()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
inBattle():boolean {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? !!this.bb!.readInt8(this.bb_pos + offset) : false;
}
number():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
numberLeft():bigint {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
loadQuantity():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0;
}
tech(index: number, obj?:TechEntry):TechEntry|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? (obj || new TechEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
techLength():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
race():string|null
race(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
race(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;
}
className():string|null
className(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
className(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;
}
loadType():string|null
loadType(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
loadType(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;
}
static startBattleReportGroup(builder:flatbuffers.Builder) {
builder.startObject(8);
}
static addInBattle(builder:flatbuffers.Builder, inBattle:boolean) {
builder.addFieldInt8(0, +inBattle, +false);
}
static addNumber(builder:flatbuffers.Builder, number:bigint) {
builder.addFieldInt64(1, number, BigInt('0'));
}
static addNumberLeft(builder:flatbuffers.Builder, numberLeft:bigint) {
builder.addFieldInt64(2, numberLeft, BigInt('0'));
}
static addLoadQuantity(builder:flatbuffers.Builder, loadQuantity:number) {
builder.addFieldFloat32(3, loadQuantity, 0.0);
}
static addTech(builder:flatbuffers.Builder, techOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, techOffset, 0);
}
static createTechVector(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 startTechVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addRace(builder:flatbuffers.Builder, raceOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, raceOffset, 0);
}
static addClassName(builder:flatbuffers.Builder, classNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(6, classNameOffset, 0);
}
static addLoadType(builder:flatbuffers.Builder, loadTypeOffset:flatbuffers.Offset) {
builder.addFieldOffset(7, loadTypeOffset, 0);
}
static endBattleReportGroup(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createBattleReportGroup(builder:flatbuffers.Builder, inBattle:boolean, number:bigint, numberLeft:bigint, loadQuantity:number, techOffset:flatbuffers.Offset, raceOffset:flatbuffers.Offset, classNameOffset:flatbuffers.Offset, loadTypeOffset:flatbuffers.Offset):flatbuffers.Offset {
BattleReportGroup.startBattleReportGroup(builder);
BattleReportGroup.addInBattle(builder, inBattle);
BattleReportGroup.addNumber(builder, number);
BattleReportGroup.addNumberLeft(builder, numberLeft);
BattleReportGroup.addLoadQuantity(builder, loadQuantity);
BattleReportGroup.addTech(builder, techOffset);
BattleReportGroup.addRace(builder, raceOffset);
BattleReportGroup.addClassName(builder, classNameOffset);
BattleReportGroup.addLoadType(builder, loadTypeOffset);
return BattleReportGroup.endBattleReportGroup(builder);
}
unpack(): BattleReportGroupT {
return new BattleReportGroupT(
this.inBattle(),
this.number(),
this.numberLeft(),
this.loadQuantity(),
this.bb!.createObjList<TechEntry, TechEntryT>(this.tech.bind(this), this.techLength()),
this.race(),
this.className(),
this.loadType()
);
}
unpackTo(_o: BattleReportGroupT): void {
_o.inBattle = this.inBattle();
_o.number = this.number();
_o.numberLeft = this.numberLeft();
_o.loadQuantity = this.loadQuantity();
_o.tech = this.bb!.createObjList<TechEntry, TechEntryT>(this.tech.bind(this), this.techLength());
_o.race = this.race();
_o.className = this.className();
_o.loadType = this.loadType();
}
}
export class BattleReportGroupT implements flatbuffers.IGeneratedObject {
constructor(
public inBattle: boolean = false,
public number: bigint = BigInt('0'),
public numberLeft: bigint = BigInt('0'),
public loadQuantity: number = 0.0,
public tech: (TechEntryT)[] = [],
public race: string|Uint8Array|null = null,
public className: string|Uint8Array|null = null,
public loadType: string|Uint8Array|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const tech = BattleReportGroup.createTechVector(builder, builder.createObjectOffsetList(this.tech));
const race = (this.race !== null ? builder.createString(this.race!) : 0);
const className = (this.className !== null ? builder.createString(this.className!) : 0);
const loadType = (this.loadType !== null ? builder.createString(this.loadType!) : 0);
return BattleReportGroup.createBattleReportGroup(builder,
this.inBattle,
this.number,
this.numberLeft,
this.loadQuantity,
tech,
race,
className,
loadType
);
}
}
@@ -0,0 +1,215 @@
// 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 { BattleActionReport, BattleActionReportT } from '../battle/battle-action-report.js';
import { RaceEntry, RaceEntryT } from '../battle/race-entry.js';
import { ShipEntry, ShipEntryT } from '../battle/ship-entry.js';
import { UUID, UUIDT } from '../battle/uuid.js';
export class BattleReport implements flatbuffers.IUnpackableObject<BattleReportT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):BattleReport {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsBattleReport(bb:flatbuffers.ByteBuffer, obj?:BattleReport):BattleReport {
return (obj || new BattleReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsBattleReport(bb:flatbuffers.ByteBuffer, obj?:BattleReport):BattleReport {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new BattleReport()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
id(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;
}
planet():bigint {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint64(this.bb_pos + offset) : BigInt('0');
}
planetName():string|null
planetName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
planetName(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;
}
races(index: number, obj?:RaceEntry):RaceEntry|null {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? (obj || new RaceEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
racesLength():number {
const offset = this.bb!.__offset(this.bb_pos, 10);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
ships(index: number, obj?:ShipEntry):ShipEntry|null {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? (obj || new ShipEntry()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
shipsLength():number {
const offset = this.bb!.__offset(this.bb_pos, 12);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
protocol(index: number, obj?:BattleActionReport):BattleActionReport|null {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? (obj || new BattleActionReport()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
}
protocolLength():number {
const offset = this.bb!.__offset(this.bb_pos, 14);
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
}
static startBattleReport(builder:flatbuffers.Builder) {
builder.startObject(6);
}
static addId(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, idOffset, 0);
}
static addPlanet(builder:flatbuffers.Builder, planet:bigint) {
builder.addFieldInt64(1, planet, BigInt('0'));
}
static addPlanetName(builder:flatbuffers.Builder, planetNameOffset:flatbuffers.Offset) {
builder.addFieldOffset(2, planetNameOffset, 0);
}
static addRaces(builder:flatbuffers.Builder, racesOffset:flatbuffers.Offset) {
builder.addFieldOffset(3, racesOffset, 0);
}
static createRacesVector(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 startRacesVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addShips(builder:flatbuffers.Builder, shipsOffset:flatbuffers.Offset) {
builder.addFieldOffset(4, shipsOffset, 0);
}
static createShipsVector(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 startShipsVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static addProtocol(builder:flatbuffers.Builder, protocolOffset:flatbuffers.Offset) {
builder.addFieldOffset(5, protocolOffset, 0);
}
static createProtocolVector(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 startProtocolVector(builder:flatbuffers.Builder, numElems:number) {
builder.startVector(4, numElems, 4);
}
static endBattleReport(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // id
return offset;
}
static finishBattleReportBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) {
builder.finish(offset);
}
static finishSizePrefixedBattleReportBuffer(builder:flatbuffers.Builder, offset:flatbuffers.Offset) {
builder.finish(offset, undefined, true);
}
static createBattleReport(builder:flatbuffers.Builder, idOffset:flatbuffers.Offset, planet:bigint, planetNameOffset:flatbuffers.Offset, racesOffset:flatbuffers.Offset, shipsOffset:flatbuffers.Offset, protocolOffset:flatbuffers.Offset):flatbuffers.Offset {
BattleReport.startBattleReport(builder);
BattleReport.addId(builder, idOffset);
BattleReport.addPlanet(builder, planet);
BattleReport.addPlanetName(builder, planetNameOffset);
BattleReport.addRaces(builder, racesOffset);
BattleReport.addShips(builder, shipsOffset);
BattleReport.addProtocol(builder, protocolOffset);
return BattleReport.endBattleReport(builder);
}
unpack(): BattleReportT {
return new BattleReportT(
(this.id() !== null ? this.id()!.unpack() : null),
this.planet(),
this.planetName(),
this.bb!.createObjList<RaceEntry, RaceEntryT>(this.races.bind(this), this.racesLength()),
this.bb!.createObjList<ShipEntry, ShipEntryT>(this.ships.bind(this), this.shipsLength()),
this.bb!.createObjList<BattleActionReport, BattleActionReportT>(this.protocol.bind(this), this.protocolLength())
);
}
unpackTo(_o: BattleReportT): void {
_o.id = (this.id() !== null ? this.id()!.unpack() : null);
_o.planet = this.planet();
_o.planetName = this.planetName();
_o.races = this.bb!.createObjList<RaceEntry, RaceEntryT>(this.races.bind(this), this.racesLength());
_o.ships = this.bb!.createObjList<ShipEntry, ShipEntryT>(this.ships.bind(this), this.shipsLength());
_o.protocol = this.bb!.createObjList<BattleActionReport, BattleActionReportT>(this.protocol.bind(this), this.protocolLength());
}
}
export class BattleReportT implements flatbuffers.IGeneratedObject {
constructor(
public id: UUIDT|null = null,
public planet: bigint = BigInt('0'),
public planetName: string|Uint8Array|null = null,
public races: (RaceEntryT)[] = [],
public ships: (ShipEntryT)[] = [],
public protocol: (BattleActionReportT)[] = []
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const planetName = (this.planetName !== null ? builder.createString(this.planetName!) : 0);
const races = BattleReport.createRacesVector(builder, builder.createObjectOffsetList(this.races));
const ships = BattleReport.createShipsVector(builder, builder.createObjectOffsetList(this.ships));
const protocol = BattleReport.createProtocolVector(builder, builder.createObjectOffsetList(this.protocol));
return BattleReport.createBattleReport(builder,
(this.id !== null ? this.id!.pack(builder) : 0),
this.planet,
planetName,
races,
ships,
protocol
);
}
}
@@ -0,0 +1,99 @@
// 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 '../battle/uuid.js';
export class GameBattleRequest implements flatbuffers.IUnpackableObject<GameBattleRequestT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):GameBattleRequest {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsGameBattleRequest(bb:flatbuffers.ByteBuffer, obj?:GameBattleRequest):GameBattleRequest {
return (obj || new GameBattleRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsGameBattleRequest(bb:flatbuffers.ByteBuffer, obj?:GameBattleRequest):GameBattleRequest {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new GameBattleRequest()).__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;
}
turn():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readUint32(this.bb_pos + offset) : 0;
}
battleId(obj?:UUID):UUID|null {
const offset = this.bb!.__offset(this.bb_pos, 8);
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
}
static startGameBattleRequest(builder:flatbuffers.Builder) {
builder.startObject(3);
}
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(0, gameIdOffset, 0);
}
static addTurn(builder:flatbuffers.Builder, turn:number) {
builder.addFieldInt32(1, turn, 0);
}
static addBattleId(builder:flatbuffers.Builder, battleIdOffset:flatbuffers.Offset) {
builder.addFieldStruct(2, battleIdOffset, 0);
}
static endGameBattleRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 4) // game_id
builder.requiredField(offset, 8) // battle_id
return offset;
}
unpack(): GameBattleRequestT {
return new GameBattleRequestT(
(this.gameId() !== null ? this.gameId()!.unpack() : null),
this.turn(),
(this.battleId() !== null ? this.battleId()!.unpack() : null)
);
}
unpackTo(_o: GameBattleRequestT): void {
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
_o.turn = this.turn();
_o.battleId = (this.battleId() !== null ? this.battleId()!.unpack() : null);
}
}
export class GameBattleRequestT implements flatbuffers.IGeneratedObject {
constructor(
public gameId: UUIDT|null = null,
public turn: number = 0,
public battleId: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
GameBattleRequest.startGameBattleRequest(builder);
GameBattleRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
GameBattleRequest.addTurn(builder, this.turn);
GameBattleRequest.addBattleId(builder, (this.battleId !== null ? this.battleId!.pack(builder) : 0));
return GameBattleRequest.endGameBattleRequest(builder);
}
}
@@ -0,0 +1,85 @@
// 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 '../battle/uuid.js';
export class RaceEntry implements flatbuffers.IUnpackableObject<RaceEntryT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):RaceEntry {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsRaceEntry(bb:flatbuffers.ByteBuffer, obj?:RaceEntry):RaceEntry {
return (obj || new RaceEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsRaceEntry(bb:flatbuffers.ByteBuffer, obj?:RaceEntry):RaceEntry {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new RaceEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
key():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
value(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 startRaceEntry(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addKey(builder:flatbuffers.Builder, key:bigint) {
builder.addFieldInt64(0, key, BigInt('0'));
}
static addValue(builder:flatbuffers.Builder, valueOffset:flatbuffers.Offset) {
builder.addFieldStruct(1, valueOffset, 0);
}
static endRaceEntry(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
builder.requiredField(offset, 6) // value
return offset;
}
unpack(): RaceEntryT {
return new RaceEntryT(
this.key(),
(this.value() !== null ? this.value()!.unpack() : null)
);
}
unpackTo(_o: RaceEntryT): void {
_o.key = this.key();
_o.value = (this.value() !== null ? this.value()!.unpack() : null);
}
}
export class RaceEntryT implements flatbuffers.IGeneratedObject {
constructor(
public key: bigint = BigInt('0'),
public value: UUIDT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
RaceEntry.startRaceEntry(builder);
RaceEntry.addKey(builder, this.key);
RaceEntry.addValue(builder, (this.value !== null ? this.value!.pack(builder) : 0));
return RaceEntry.endRaceEntry(builder);
}
}
@@ -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 { BattleReportGroup, BattleReportGroupT } from '../battle/battle-report-group.js';
export class ShipEntry implements flatbuffers.IUnpackableObject<ShipEntryT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):ShipEntry {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsShipEntry(bb:flatbuffers.ByteBuffer, obj?:ShipEntry):ShipEntry {
return (obj || new ShipEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsShipEntry(bb:flatbuffers.ByteBuffer, obj?:ShipEntry):ShipEntry {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new ShipEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
key():bigint {
const offset = this.bb!.__offset(this.bb_pos, 4);
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
}
value(obj?:BattleReportGroup):BattleReportGroup|null {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? (obj || new BattleReportGroup()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
}
static startShipEntry(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addKey(builder:flatbuffers.Builder, key:bigint) {
builder.addFieldInt64(0, key, BigInt('0'));
}
static addValue(builder:flatbuffers.Builder, valueOffset:flatbuffers.Offset) {
builder.addFieldOffset(1, valueOffset, 0);
}
static endShipEntry(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
unpack(): ShipEntryT {
return new ShipEntryT(
this.key(),
(this.value() !== null ? this.value()!.unpack() : null)
);
}
unpackTo(_o: ShipEntryT): void {
_o.key = this.key();
_o.value = (this.value() !== null ? this.value()!.unpack() : null);
}
}
export class ShipEntryT implements flatbuffers.IGeneratedObject {
constructor(
public key: bigint = BigInt('0'),
public value: BattleReportGroupT|null = null
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const value = (this.value !== null ? this.value!.pack(builder) : 0);
ShipEntry.startShipEntry(builder);
ShipEntry.addKey(builder, this.key);
ShipEntry.addValue(builder, value);
return ShipEntry.endShipEntry(builder);
}
}
@@ -0,0 +1,92 @@
// 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 TechEntry implements flatbuffers.IUnpackableObject<TechEntryT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):TechEntry {
this.bb_pos = i;
this.bb = bb;
return this;
}
static getRootAsTechEntry(bb:flatbuffers.ByteBuffer, obj?:TechEntry):TechEntry {
return (obj || new TechEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
static getSizePrefixedRootAsTechEntry(bb:flatbuffers.ByteBuffer, obj?:TechEntry):TechEntry {
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
return (obj || new TechEntry()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
}
key():string|null
key(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
key(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;
}
value():number {
const offset = this.bb!.__offset(this.bb_pos, 6);
return offset ? this.bb!.readFloat32(this.bb_pos + offset) : 0.0;
}
static startTechEntry(builder:flatbuffers.Builder) {
builder.startObject(2);
}
static addKey(builder:flatbuffers.Builder, keyOffset:flatbuffers.Offset) {
builder.addFieldOffset(0, keyOffset, 0);
}
static addValue(builder:flatbuffers.Builder, value:number) {
builder.addFieldFloat32(1, value, 0.0);
}
static endTechEntry(builder:flatbuffers.Builder):flatbuffers.Offset {
const offset = builder.endObject();
return offset;
}
static createTechEntry(builder:flatbuffers.Builder, keyOffset:flatbuffers.Offset, value:number):flatbuffers.Offset {
TechEntry.startTechEntry(builder);
TechEntry.addKey(builder, keyOffset);
TechEntry.addValue(builder, value);
return TechEntry.endTechEntry(builder);
}
unpack(): TechEntryT {
return new TechEntryT(
this.key(),
this.value()
);
}
unpackTo(_o: TechEntryT): void {
_o.key = this.key();
_o.value = this.value();
}
}
export class TechEntryT implements flatbuffers.IGeneratedObject {
constructor(
public key: string|Uint8Array|null = null,
public value: number = 0.0
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
const key = (this.key !== null ? builder.createString(this.key!) : 0);
return TechEntry.createTechEntry(builder,
key,
this.value
);
}
}
@@ -0,0 +1,65 @@
// 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 UUID implements flatbuffers.IUnpackableObject<UUIDT> {
bb: flatbuffers.ByteBuffer|null = null;
bb_pos = 0;
__init(i:number, bb:flatbuffers.ByteBuffer):UUID {
this.bb_pos = i;
this.bb = bb;
return this;
}
hi():bigint {
return this.bb!.readUint64(this.bb_pos);
}
lo():bigint {
return this.bb!.readUint64(this.bb_pos + 8);
}
static sizeOf():number {
return 16;
}
static createUUID(builder:flatbuffers.Builder, hi: bigint, lo: bigint):flatbuffers.Offset {
builder.prep(8, 16);
builder.writeInt64(BigInt(lo ?? 0));
builder.writeInt64(BigInt(hi ?? 0));
return builder.offset();
}
unpack(): UUIDT {
return new UUIDT(
this.hi(),
this.lo()
);
}
unpackTo(_o: UUIDT): void {
_o.hi = this.hi();
_o.lo = this.lo();
}
}
export class UUIDT implements flatbuffers.IGeneratedObject {
constructor(
public hi: bigint = BigInt('0'),
public lo: bigint = BigInt('0')
){}
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
return UUID.createUUID(builder,
this.hi,
this.lo
);
}
}
@@ -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
);
}
}

Some files were not shown because too many files have changed in this diff Show More