From 7b43ce5844ceadbdfd04761fc8ada1d6ce113a10 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:07:48 +0200 Subject: [PATCH 01/11] Phase 28 (Step 1): backend support for race-name mail send 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) --- backend/internal/diplomail/README.md | 23 +++- backend/internal/diplomail/admin_send.go | 35 ++++-- .../internal/diplomail/diplomail_e2e_test.go | 108 ++++++++++++++++++ backend/internal/diplomail/service.go | 61 +++++++++- backend/internal/diplomail/store.go | 10 +- backend/internal/diplomail/types.go | 50 +++++--- .../jet/backend/model/diplomail_messages.go | 1 + .../jet/backend/table/diplomail_messages.go | 7 +- .../postgres/migrations/00001_init.sql | 12 ++ .../server/handlers_admin_diplomail.go | 27 +++-- backend/internal/server/handlers_user_mail.go | 85 ++++++++------ backend/openapi.yaml | 40 ++++++- 12 files changed, 372 insertions(+), 87 deletions(-) diff --git a/backend/internal/diplomail/README.md b/backend/internal/diplomail/README.md index 581d673..4e86b01 100644 --- a/backend/internal/diplomail/README.md +++ b/backend/internal/diplomail/README.md @@ -26,7 +26,10 @@ Three Postgres tables in the `backend` schema: - `diplomail_messages` — one row per send (personal, admin, or system). Captures `game_name` and IP at insert time so audit - rendering survives renames and purges. + rendering survives renames and purges. The `sender_race_name` + column snapshots the sender's race in the game at send time when + the sender is a player with an active membership; the in-game UI + keys per-race thread grouping on this column. - `diplomail_recipients` — one row per (message, recipient). Holds per-user `read_at`, `deleted_at`, `delivered_at`, `notified_at` state. Snapshot fields (`recipient_user_name`, @@ -72,6 +75,24 @@ mail to every active member; `Service.changeMembershipStatus` / `detector.LanguageDetector` (default: `whatlanggo`, body-only, ≥ 25 runes; shorter bodies stay `und`). +## Recipient selection + +`POST /messages` and `POST /admin` (when `target="user"`) accept the +recipient identifier in one of two shapes: + +- `recipient_user_id` (uuid) — explicit user lookup; the recipient + may be any active member of the game. +- `recipient_race_name` (string) — resolves to the active member + with this race name in the game. Race names are unique by lobby + invariant; lobby-removed and blocked members cannot be reached + through the race-name shortcut (they no longer appear in the + active scope). Exactly one of the two fields must be supplied; + supplying both, or neither, returns `invalid_request`. + +The race-name path lets the in-game UI compose mail directly off +the engine's `report.races[]` view without an extra membership +round-trip. + ## Translation Stage D adds a lazy translation cache. When a recipient reads a diff --git a/backend/internal/diplomail/admin_send.go b/backend/internal/diplomail/admin_send.go index 0b4c5c8..eb2eb9c 100644 --- a/backend/internal/diplomail/admin_send.go +++ b/backend/internal/diplomail/admin_send.go @@ -29,7 +29,11 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp return Message{}, Recipient{}, err } - recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, in.RecipientUserID) + recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName) + if err != nil { + return Message{}, Recipient{}, err + } + recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, recipientID) if err != nil { if errors.Is(err, ErrNotFound) { return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not a member of the game", ErrForbidden) @@ -37,7 +41,7 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp return Message{}, Recipient{}, fmt.Errorf("diplomail: load admin recipient: %w", err) } - msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername, + msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername, recipient.GameID, recipient.GameName, subject, body, in.SenderIP, BroadcastScopeSingle) if err != nil { return Message{}, Recipient{}, err @@ -84,7 +88,7 @@ func (s *Service) SendAdminBroadcast(ctx context.Context, in SendAdminBroadcastI } gameName := members[0].GameName - msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername, + msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername, in.GameID, gameName, subject, body, in.SenderIP, BroadcastScopeGameBroadcast) if err != nil { return Message{}, nil, err @@ -147,6 +151,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas } username := sender.UserName + senderRace := sender.RaceName msgInsert := MessageInsert{ MessageID: uuid.New(), GameID: in.GameID, @@ -155,6 +160,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas SenderKind: SenderKindPlayer, SenderUserID: &callerID, SenderUsername: &username, + SenderRaceName: &senderRace, SenderIP: in.SenderIP, Subject: subject, Body: body, @@ -217,7 +223,7 @@ func (s *Service) SendAdminMultiGameBroadcast(ctx context.Context, in SendMultiG zap.String("scope", scope)) continue } - msgInsert, err := s.buildAdminMessageInsert(CallerKindAdmin, nil, in.CallerUsername, + msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindAdmin, nil, in.CallerUsername, game.GameID, game.GameName, subject, body, in.SenderIP, BroadcastScopeMultiGameBroadcast) if err != nil { return nil, 0, err @@ -356,7 +362,7 @@ func (s *Service) publishGameLifecycle(ctx context.Context, ev LifecycleEvent) e gameName := members[0].GameName subject, body := renderGameLifecycle(ev.Kind, gameName, ev.Actor, ev.Reason) - msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "", + msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "", ev.GameID, gameName, subject, body, "", BroadcastScopeGameBroadcast) if err != nil { return err @@ -385,7 +391,7 @@ func (s *Service) publishMembershipLifecycle(ctx context.Context, ev LifecycleEv } subject, body := renderMembershipLifecycle(ev.Kind, target.GameName, ev.Actor, ev.Reason) - msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "", + msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "", ev.GameID, target.GameName, subject, body, "", BroadcastScopeSingle) if err != nil { return err @@ -417,10 +423,12 @@ func (s *Service) prepareContent(subject, body string) (string, string, error) { // for every admin-kind send. The CHECK constraint maps sender // shapes: // -// sender_kind='player' → CallerKind owner; sender_user_id set +// sender_kind='player' → CallerKind owner; sender_user_id set, +// sender_race_name resolved from +// Memberships.GetActiveMembership // sender_kind='admin' → CallerKind admin; sender_user_id nil // sender_kind='system' → CallerKind system; sender_username nil -func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.UUID, callerUsername string, +func (s *Service) buildAdminMessageInsert(ctx context.Context, callerKind string, callerUserID *uuid.UUID, callerUsername string, gameID uuid.UUID, gameName, subject, body, senderIP, scope string) (MessageInsert, error) { out := MessageInsert{ MessageID: uuid.New(), @@ -443,6 +451,17 @@ func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid. out.SenderKind = SenderKindPlayer out.SenderUserID = &uid out.SenderUsername = &uname + // Owner race snapshot is best-effort: a private-game owner who + // has an active membership in their own game contributes a + // race name; an owner who is not a current member (or whose + // membership is removed/blocked) leaves the field nil. The + // CHECK constraint accepts both shapes for sender_kind='player'. + if ownerMember, err := s.deps.Memberships.GetActiveMembership(ctx, gameID, uid); err == nil { + race := ownerMember.RaceName + out.SenderRaceName = &race + } else if !errors.Is(err, ErrNotFound) { + return MessageInsert{}, fmt.Errorf("diplomail: load owner membership: %w", err) + } case CallerKindAdmin: uname := callerUsername out.SenderKind = SenderKindAdmin diff --git a/backend/internal/diplomail/diplomail_e2e_test.go b/backend/internal/diplomail/diplomail_e2e_test.go index 3f85830..71511b8 100644 --- a/backend/internal/diplomail/diplomail_e2e_test.go +++ b/backend/internal/diplomail/diplomail_e2e_test.go @@ -369,6 +369,114 @@ func TestDiplomailPersonalFlow(t *testing.T) { } } +// TestDiplomailPersonalByRaceName exercises the Phase 28 contract: the +// UI passes a recipient race name (read out of the game report); the +// service resolves it to the active member with that race name and +// snapshots the sender's race onto the message row. Error cases cover +// the validation rules baked into the wire schema. +func TestDiplomailPersonalByRaceName(t *testing.T) { + db := startPostgres(t) + ctx := context.Background() + + gameID := uuid.New() + sender := uuid.New() + recipient := uuid.New() + kicked := uuid.New() + seedAccount(t, db, sender) + seedAccount(t, db, recipient) + seedAccount(t, db, kicked) + seedGame(t, db, gameID, "Race-Name Resolution Game") + + lookup := &staticMembershipLookup{ + rows: map[[2]uuid.UUID]diplomail.ActiveMembership{ + {gameID, sender}: { + UserID: sender, GameID: gameID, GameName: "Race-Name Resolution Game", + UserName: "sender", RaceName: "Senders", + }, + {gameID, recipient}: { + UserID: recipient, GameID: gameID, GameName: "Race-Name Resolution Game", + UserName: "recipient", RaceName: "Receivers", + }, + }, + inactive: map[[2]uuid.UUID]diplomail.MemberSnapshot{ + {gameID, kicked}: { + UserID: kicked, GameID: gameID, GameName: "Race-Name Resolution Game", + UserName: "kicked", RaceName: "Departed", Status: "removed", + }, + }, + } + svc := diplomail.NewService(diplomail.Deps{ + Store: diplomail.NewStore(db), + Memberships: lookup, + Notification: &recordingPublisher{}, + Config: config.DiplomailConfig{ + MaxBodyBytes: 4096, + MaxSubjectBytes: 256, + }, + }) + + // Happy path: race name resolves and sender_race_name is snapshotted. + msg, rcpt, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{ + GameID: gameID, + SenderUserID: sender, + RecipientRaceName: "Receivers", + Subject: "Trade", + Body: "Care to talk?", + SenderIP: "203.0.113.4", + }) + if err != nil { + t.Fatalf("send by race name: %v", err) + } + if rcpt.UserID != recipient { + t.Fatalf("recipient = %s, want %s", rcpt.UserID, recipient) + } + if msg.SenderRaceName == nil || *msg.SenderRaceName != "Senders" { + t.Fatalf("sender_race_name = %v, want \"Senders\"", msg.SenderRaceName) + } + + // Both identifiers supplied → invalid_request. + if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{ + GameID: gameID, + SenderUserID: sender, + RecipientUserID: recipient, + RecipientRaceName: "Receivers", + Body: "x", + }); !errors.Is(err, diplomail.ErrInvalidInput) { + t.Fatalf("dual identifier: %v, want ErrInvalidInput", err) + } + + // Neither identifier supplied → invalid_request. + if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{ + GameID: gameID, + SenderUserID: sender, + Body: "x", + }); !errors.Is(err, diplomail.ErrInvalidInput) { + t.Fatalf("no identifier: %v, want ErrInvalidInput", err) + } + + // Race name with no matching active member → invalid_request. + if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{ + GameID: gameID, + SenderUserID: sender, + RecipientRaceName: "Strangers", + Body: "x", + }); !errors.Is(err, diplomail.ErrInvalidInput) { + t.Fatalf("unknown race: %v, want ErrInvalidInput", err) + } + + // Race name of a lobby-removed member → invalid_request (the + // active-only scope filters them out; the lookup never returns + // them). + if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{ + GameID: gameID, + SenderUserID: sender, + RecipientRaceName: "Departed", + Body: "x", + }); !errors.Is(err, diplomail.ErrInvalidInput) { + t.Fatalf("kicked race: %v, want ErrInvalidInput", err) + } +} + func TestDiplomailRejectsNonActiveSender(t *testing.T) { db := startPostgres(t) ctx := context.Background() diff --git a/backend/internal/diplomail/service.go b/backend/internal/diplomail/service.go index b6365a7..7c350b5 100644 --- a/backend/internal/diplomail/service.go +++ b/backend/internal/diplomail/service.go @@ -32,15 +32,20 @@ const previewMaxRunes = 120 // ErrForbidden; the inserted Message is never persisted in those // cases. func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Message, Recipient, error) { - if in.SenderUserID == in.RecipientUserID { - return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput) - } subject := strings.TrimRight(in.Subject, " \t") body := strings.TrimRight(in.Body, " \t\n") if err := s.validateContent(subject, body); err != nil { return Message{}, Recipient{}, err } + recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName) + if err != nil { + return Message{}, Recipient{}, err + } + if in.SenderUserID == recipientID { + return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput) + } + sender, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.SenderUserID) if err != nil { if errors.Is(err, ErrNotFound) { @@ -48,7 +53,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa } return Message{}, Recipient{}, fmt.Errorf("diplomail: load sender membership: %w", err) } - recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.RecipientUserID) + recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, recipientID) if err != nil { if errors.Is(err, ErrNotFound) { return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not an active member of the game", ErrForbidden) @@ -57,14 +62,17 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa } username := sender.UserName + senderRace := sender.RaceName + senderUserID := in.SenderUserID msgInsert := MessageInsert{ MessageID: uuid.New(), GameID: in.GameID, GameName: sender.GameName, Kind: KindPersonal, SenderKind: SenderKindPlayer, - SenderUserID: &in.SenderUserID, + SenderUserID: &senderUserID, SenderUsername: &username, + SenderRaceName: &senderRace, SenderIP: in.SenderIP, Subject: subject, Body: body, @@ -75,7 +83,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa rcptInsert := buildRecipientInsert( msgInsert.MessageID, MemberSnapshot{ - UserID: in.RecipientUserID, + UserID: recipientID, GameID: in.GameID, GameName: recipient.GameName, UserName: recipient.UserName, @@ -101,6 +109,47 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa return msg, recipients[0], nil } +// resolveActiveRecipient turns a (user_id, race_name) pair into the +// canonical user id of an active member of gameID. Exactly one of the +// two inputs must be set; both-set or both-empty returns +// ErrInvalidInput. Race-name resolution is restricted to the active +// scope so lobby-removed and blocked members cannot be reached +// through the race-name shortcut. ErrInvalidInput is also returned +// when the race name matches zero members; ErrForbidden when the +// race name matches more than one active row (defence in depth — race +// names are unique within a game by lobby invariant). +func (s *Service) resolveActiveRecipient(ctx context.Context, gameID uuid.UUID, byUserID uuid.UUID, byRaceName string) (uuid.UUID, error) { + byRaceName = strings.TrimSpace(byRaceName) + hasUser := byUserID != uuid.Nil + hasRace := byRaceName != "" + switch { + case hasUser && hasRace: + return uuid.Nil, fmt.Errorf("%w: only one of recipient_user_id, recipient_race_name may be supplied", ErrInvalidInput) + case !hasUser && !hasRace: + return uuid.Nil, fmt.Errorf("%w: recipient_user_id or recipient_race_name must be supplied", ErrInvalidInput) + case hasUser: + return byUserID, nil + } + members, err := s.deps.Memberships.ListMembers(ctx, gameID, RecipientScopeActive) + if err != nil { + return uuid.Nil, fmt.Errorf("diplomail: list active members for race lookup: %w", err) + } + var found []MemberSnapshot + for _, m := range members { + if m.RaceName == byRaceName { + found = append(found, m) + } + } + switch len(found) { + case 0: + return uuid.Nil, fmt.Errorf("%w: no active member with race %q in this game", ErrInvalidInput, byRaceName) + case 1: + return found[0].UserID, nil + default: + return uuid.Nil, fmt.Errorf("%w: race %q matches multiple active members", ErrForbidden, byRaceName) + } +} + // GetMessage returns the InboxEntry for messageID addressed to // userID. ErrNotFound is returned when the caller is not a recipient // of the message — handlers translate that to 404 so the existence diff --git a/backend/internal/diplomail/store.go b/backend/internal/diplomail/store.go index e6364cf..6d4f5c9 100644 --- a/backend/internal/diplomail/store.go +++ b/backend/internal/diplomail/store.go @@ -31,7 +31,7 @@ func messageColumns() postgres.ColumnList { m := table.DiplomailMessages return postgres.ColumnList{ m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind, - m.SenderUserID, m.SenderUsername, m.SenderIP, + m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP, m.Subject, m.Body, m.BodyLang, m.BroadcastScope, m.CreatedAt, } } @@ -59,6 +59,7 @@ type MessageInsert struct { SenderKind string SenderUserID *uuid.UUID SenderUsername *string + SenderRaceName *string SenderIP string Subject string Body string @@ -101,7 +102,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse m := table.DiplomailMessages msgStmt := m.INSERT( m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind, - m.SenderUserID, m.SenderUsername, m.SenderIP, + m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP, m.Subject, m.Body, m.BodyLang, m.BroadcastScope, ).VALUES( msg.MessageID, @@ -111,6 +112,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse msg.SenderKind, uuidPtrArg(msg.SenderUserID), stringPtrArg(msg.SenderUsername), + stringPtrArg(msg.SenderRaceName), msg.SenderIP, msg.Subject, msg.Body, @@ -737,6 +739,10 @@ func messageFromModel(row model.DiplomailMessages) Message { name := *row.SenderUsername out.SenderUsername = &name } + if row.SenderRaceName != nil { + name := *row.SenderRaceName + out.SenderRaceName = &name + } return out } diff --git a/backend/internal/diplomail/types.go b/backend/internal/diplomail/types.go index f1b29f7..52bfb45 100644 --- a/backend/internal/diplomail/types.go +++ b/backend/internal/diplomail/types.go @@ -23,6 +23,11 @@ type Message struct { SenderKind string SenderUserID *uuid.UUID SenderUsername *string + // SenderRaceName carries the snapshot of the sender's race in the + // game at send time. Non-nil for sender_kind='player' rows, nil + // for admin and system. The in-game mail UI groups personal + // threads by this name (Phase 28). + SenderRaceName *string SenderIP string Subject string Body string @@ -92,16 +97,19 @@ type Translation struct { } // SendPersonalInput is the request payload for SendPersonal: the -// caller sending a single-recipient personal message. Validation -// (active membership, body length, etc.) is performed inside the -// service. +// caller sending a single-recipient personal message. Exactly one of +// RecipientUserID and RecipientRaceName must be non-zero; the +// service resolves a non-empty RecipientRaceName to the active +// member with that race in the game. Other validation (active +// membership, body length, etc.) is performed inside the service. type SendPersonalInput struct { - GameID uuid.UUID - SenderUserID uuid.UUID - RecipientUserID uuid.UUID - Subject string - Body string - SenderIP string + GameID uuid.UUID + SenderUserID uuid.UUID + RecipientUserID uuid.UUID + RecipientRaceName string + Subject string + Body string + SenderIP string } // CallerKind enumerates the privileged sender roles for admin-kind @@ -116,17 +124,21 @@ const ( // SendAdminPersonalInput is the request payload for an owner / // admin / system sending an admin-kind message to a single -// recipient. Authorization (owner-vs-admin distinction) is enforced -// by the HTTP layer; the service trusts the caller designation. +// recipient. Exactly one of RecipientUserID and RecipientRaceName +// must be non-zero; the service resolves a non-empty +// RecipientRaceName to the active member with that race in the +// game. Authorization (owner-vs-admin distinction) is enforced by +// the HTTP layer; the service trusts the caller designation. type SendAdminPersonalInput struct { - GameID uuid.UUID - CallerKind string - CallerUserID *uuid.UUID - CallerUsername string - RecipientUserID uuid.UUID - Subject string - Body string - SenderIP string + GameID uuid.UUID + CallerKind string + CallerUserID *uuid.UUID + CallerUsername string + RecipientUserID uuid.UUID + RecipientRaceName string + Subject string + Body string + SenderIP string } // SendAdminBroadcastInput is the request payload for an owner / diff --git a/backend/internal/postgres/jet/backend/model/diplomail_messages.go b/backend/internal/postgres/jet/backend/model/diplomail_messages.go index c2939cc..8b82732 100644 --- a/backend/internal/postgres/jet/backend/model/diplomail_messages.go +++ b/backend/internal/postgres/jet/backend/model/diplomail_messages.go @@ -20,6 +20,7 @@ type DiplomailMessages struct { SenderKind string SenderUserID *uuid.UUID SenderUsername *string + SenderRaceName *string SenderIP string Subject string Body string diff --git a/backend/internal/postgres/jet/backend/table/diplomail_messages.go b/backend/internal/postgres/jet/backend/table/diplomail_messages.go index 902e05c..afd403f 100644 --- a/backend/internal/postgres/jet/backend/table/diplomail_messages.go +++ b/backend/internal/postgres/jet/backend/table/diplomail_messages.go @@ -24,6 +24,7 @@ type diplomailMessagesTable struct { SenderKind postgres.ColumnString SenderUserID postgres.ColumnString SenderUsername postgres.ColumnString + SenderRaceName postgres.ColumnString SenderIP postgres.ColumnString Subject postgres.ColumnString Body postgres.ColumnString @@ -78,14 +79,15 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai SenderKindColumn = postgres.StringColumn("sender_kind") SenderUserIDColumn = postgres.StringColumn("sender_user_id") SenderUsernameColumn = postgres.StringColumn("sender_username") + SenderRaceNameColumn = postgres.StringColumn("sender_race_name") SenderIPColumn = postgres.StringColumn("sender_ip") SubjectColumn = postgres.StringColumn("subject") BodyColumn = postgres.StringColumn("body") BodyLangColumn = postgres.StringColumn("body_lang") BroadcastScopeColumn = postgres.StringColumn("broadcast_scope") CreatedAtColumn = postgres.TimestampzColumn("created_at") - allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn} - mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn} + allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn} + mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn} defaultColumns = postgres.ColumnList{SenderIPColumn, SubjectColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn} ) @@ -100,6 +102,7 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai SenderKind: SenderKindColumn, SenderUserID: SenderUserIDColumn, SenderUsername: SenderUsernameColumn, + SenderRaceName: SenderRaceNameColumn, SenderIP: SenderIPColumn, Subject: SubjectColumn, Body: BodyColumn, diff --git a/backend/internal/postgres/migrations/00001_init.sql b/backend/internal/postgres/migrations/00001_init.sql index c749749..afbb939 100644 --- a/backend/internal/postgres/migrations/00001_init.sql +++ b/backend/internal/postgres/migrations/00001_init.sql @@ -683,6 +683,11 @@ CREATE TABLE diplomail_messages ( sender_kind text NOT NULL, sender_user_id uuid, sender_username text, + -- sender_race_name is the immutable snapshot of the sender's race + -- in this game, captured at insert time when sender_kind='player'. + -- Admin and system messages carry NULL. The Phase 28 mail UI keys + -- per-race threading on this column. + sender_race_name text, sender_ip text NOT NULL DEFAULT '', subject text NOT NULL DEFAULT '', body text NOT NULL, @@ -698,6 +703,13 @@ CREATE TABLE diplomail_messages ( (sender_kind = 'admin' AND sender_user_id IS NULL AND sender_username IS NOT NULL) OR (sender_kind = 'system' AND sender_user_id IS NULL AND sender_username IS NULL) ), + -- sender_race_name is only meaningful for player senders. Admin + -- and system rows never carry a race; player rows carry one when + -- the sender has an active membership at send time (a non-playing + -- private-game owner may legitimately have none). + CONSTRAINT diplomail_messages_sender_race_chk CHECK ( + sender_kind = 'player' OR sender_race_name IS NULL + ), CONSTRAINT diplomail_messages_kind_sender_chk CHECK ( (kind = 'personal' AND sender_kind = 'player') OR (kind = 'admin' AND sender_kind IN ('player', 'admin', 'system')) diff --git a/backend/internal/server/handlers_admin_diplomail.go b/backend/internal/server/handlers_admin_diplomail.go index 0e1dd0a..328b902 100644 --- a/backend/internal/server/handlers_admin_diplomail.go +++ b/backend/internal/server/handlers_admin_diplomail.go @@ -60,19 +60,24 @@ func (h *AdminDiplomailHandlers) Send() gin.HandlerFunc { ctx := c.Request.Context() switch req.Target { case "", "user": - recipientID, parseErr := uuid.Parse(req.RecipientUserID) - if parseErr != nil { - httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") - return + var recipientID uuid.UUID + if req.RecipientUserID != "" { + parsed, parseErr := uuid.Parse(req.RecipientUserID) + if parseErr != nil { + httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") + return + } + recipientID = parsed } msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{ - GameID: gameID, - CallerKind: diplomail.CallerKindAdmin, - CallerUsername: username, - RecipientUserID: recipientID, - Subject: req.Subject, - Body: req.Body, - SenderIP: clientip.ExtractSourceIP(c), + GameID: gameID, + CallerKind: diplomail.CallerKindAdmin, + CallerUsername: username, + RecipientUserID: recipientID, + RecipientRaceName: req.RecipientRaceName, + Subject: req.Subject, + Body: req.Body, + SenderIP: clientip.ExtractSourceIP(c), }) if sendErr != nil { respondDiplomailError(c, h.logger, "admin mail send personal", ctx, sendErr) diff --git a/backend/internal/server/handlers_user_mail.go b/backend/internal/server/handlers_user_mail.go index 53960b6..04fd8f8 100644 --- a/backend/internal/server/handlers_user_mail.go +++ b/backend/internal/server/handlers_user_mail.go @@ -87,19 +87,24 @@ func (h *UserMailHandlers) SendPersonal() gin.HandlerFunc { httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON") return } - recipientID, err := uuid.Parse(req.RecipientUserID) - if err != nil { - httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") - return + var recipientID uuid.UUID + if req.RecipientUserID != "" { + parsed, perr := uuid.Parse(req.RecipientUserID) + if perr != nil { + httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") + return + } + recipientID = parsed } ctx := c.Request.Context() msg, rcpt, err := h.svc.SendPersonal(ctx, diplomail.SendPersonalInput{ - GameID: gameID, - SenderUserID: userID, - RecipientUserID: recipientID, - Subject: req.Subject, - Body: req.Body, - SenderIP: clientip.ExtractSourceIP(c), + GameID: gameID, + SenderUserID: userID, + RecipientUserID: recipientID, + RecipientRaceName: req.RecipientRaceName, + Subject: req.Subject, + Body: req.Body, + SenderIP: clientip.ExtractSourceIP(c), }) if err != nil { respondDiplomailError(c, h.logger, "user mail send personal", ctx, err) @@ -341,21 +346,26 @@ func (h *UserMailHandlers) SendAdmin() gin.HandlerFunc { switch req.Target { case "", "user": - recipientID, parseErr := uuid.Parse(req.RecipientUserID) - if parseErr != nil { - httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") - return + var recipientID uuid.UUID + if req.RecipientUserID != "" { + parsed, parseErr := uuid.Parse(req.RecipientUserID) + if parseErr != nil { + httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID") + return + } + recipientID = parsed } callerUserID := userID msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{ - GameID: gameID, - CallerKind: diplomail.CallerKindOwner, - CallerUserID: &callerUserID, - CallerUsername: account.UserName, - RecipientUserID: recipientID, - Subject: req.Subject, - Body: req.Body, - SenderIP: clientip.ExtractSourceIP(c), + GameID: gameID, + CallerKind: diplomail.CallerKindOwner, + CallerUserID: &callerUserID, + CallerUsername: account.UserName, + RecipientUserID: recipientID, + RecipientRaceName: req.RecipientRaceName, + Subject: req.Subject, + Body: req.Body, + SenderIP: clientip.ExtractSourceIP(c), }) if sendErr != nil { respondDiplomailError(c, h.logger, "user mail send admin personal", ctx, sendErr) @@ -449,10 +459,13 @@ func parseMessageIDParam(c *gin.Context) (uuid.UUID, bool) { } // userMailSendRequestWire mirrors the request body for SendPersonal. +// Exactly one of `recipient_user_id` and `recipient_race_name` must +// be supplied; the service rejects ambiguous and empty inputs. type userMailSendRequestWire struct { - RecipientUserID string `json:"recipient_user_id"` - Subject string `json:"subject,omitempty"` - Body string `json:"body"` + RecipientUserID string `json:"recipient_user_id,omitempty"` + RecipientRaceName string `json:"recipient_race_name,omitempty"` + Subject string `json:"subject,omitempty"` + Body string `json:"body"` } // userMailSendBroadcastRequestWire mirrors the request body for the @@ -464,15 +477,16 @@ type userMailSendBroadcastRequestWire struct { } // userMailSendAdminRequestWire mirrors the request body for the -// owner-only admin send. `target="user"` requires -// `recipient_user_id`; `target="all"` accepts the optional -// `recipients` scope (default `active`). +// owner-only admin send. `target="user"` requires exactly one of +// `recipient_user_id` and `recipient_race_name`; `target="all"` +// accepts the optional `recipients` scope (default `active`). type userMailSendAdminRequestWire struct { - Target string `json:"target"` - RecipientUserID string `json:"recipient_user_id,omitempty"` - Recipients string `json:"recipients,omitempty"` - Subject string `json:"subject,omitempty"` - Body string `json:"body"` + Target string `json:"target"` + RecipientUserID string `json:"recipient_user_id,omitempty"` + RecipientRaceName string `json:"recipient_race_name,omitempty"` + Recipients string `json:"recipients,omitempty"` + Subject string `json:"subject,omitempty"` + Body string `json:"body"` } // userMailBroadcastReceiptWire is the response shape returned after a @@ -524,6 +538,7 @@ type userMailMessageDetailWire struct { SenderKind string `json:"sender_kind"` SenderUserID *string `json:"sender_user_id,omitempty"` SenderUsername *string `json:"sender_username,omitempty"` + SenderRaceName *string `json:"sender_race_name,omitempty"` Subject string `json:"subject,omitempty"` Body string `json:"body"` BodyLang string `json:"body_lang"` @@ -597,6 +612,10 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM s := *entry.SenderUsername out.SenderUsername = &s } + if entry.SenderRaceName != nil { + s := *entry.SenderRaceName + out.SenderRaceName = &s + } if entry.Recipient.RecipientRaceName != nil { s := *entry.Recipient.RecipientRaceName out.RecipientRaceName = &s diff --git a/backend/openapi.yaml b/backend/openapi.yaml index 251fb6d..ae89e0b 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -4068,11 +4068,22 @@ components: UserMailSendRequest: type: object additionalProperties: false - required: [recipient_user_id, body] + required: [body] properties: recipient_user_id: type: string format: uuid + description: | + Either `recipient_user_id` or `recipient_race_name` must + be supplied; supplying both is rejected as + `invalid_request`. + recipient_race_name: + type: string + description: | + Resolves to the active member with this race name in the + game. Mutually exclusive with `recipient_user_id`. The + server returns `forbidden` when the matching member is no + longer active (lobby-removed / blocked). subject: type: string description: | @@ -4093,10 +4104,18 @@ components: type: string format: uuid description: | - Required when `target="user"`. Identifies the recipient - of the personal admin message; the recipient may be in - any membership status (admin notifications can reach - kicked players). + One of `recipient_user_id` and `recipient_race_name` is + required when `target="user"`. Identifies the recipient + of the personal admin message by uuid; the recipient may + be in any membership status (admin notifications can + reach kicked players when addressed by user_id). + recipient_race_name: + type: string + description: | + Optional alternative to `recipient_user_id` when + `target="user"`. Resolves to the active member with this + race name in the game; lobby-removed and blocked members + cannot be reached through the race-name shortcut. recipients: type: string enum: [active, active_and_removed, all_members] @@ -4323,6 +4342,17 @@ components: sender_username: type: string nullable: true + sender_race_name: + type: string + nullable: true + description: | + Snapshot of the sender's race name in this game at send + time. Populated when `sender_kind="player"` and the + sender had an active membership at send time; nil for + admin and system messages, and for player messages sent + by a private-game owner who was not an active member at + send time. The in-game UI keys per-race threading on this + field. subject: type: string body: -- 2.52.0 From fed282f2d287a96150bed43484ebbc73514a3450 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:21:23 +0200 Subject: [PATCH 02/11] Phase 28 (Step 2): FBS schemas + message-type constants for mail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- pkg/model/diplomail/diplomail.go | 56 +++ pkg/schema/fbs/diplomail.fbs | 196 ++++++++ pkg/schema/fbs/diplomail/AdminRequest.go | 133 ++++++ pkg/schema/fbs/diplomail/AdminResponse.go | 81 ++++ pkg/schema/fbs/diplomail/BroadcastRequest.go | 89 ++++ pkg/schema/fbs/diplomail/BroadcastResponse.go | 65 +++ pkg/schema/fbs/diplomail/DeleteRequest.go | 83 ++++ pkg/schema/fbs/diplomail/DeleteResponse.go | 65 +++ pkg/schema/fbs/diplomail/InboxRequest.go | 67 +++ pkg/schema/fbs/diplomail/InboxResponse.go | 75 +++ .../fbs/diplomail/MailBroadcastReceipt.go | 178 ++++++++ pkg/schema/fbs/diplomail/MailMessage.go | 303 +++++++++++++ .../fbs/diplomail/MailRecipientState.go | 90 ++++ pkg/schema/fbs/diplomail/MessageGetRequest.go | 83 ++++ .../fbs/diplomail/MessageGetResponse.go | 65 +++ pkg/schema/fbs/diplomail/ReadRequest.go | 83 ++++ pkg/schema/fbs/diplomail/ReadResponse.go | 65 +++ pkg/schema/fbs/diplomail/SendRequest.go | 111 +++++ pkg/schema/fbs/diplomail/SendResponse.go | 65 +++ pkg/schema/fbs/diplomail/SentRequest.go | 67 +++ pkg/schema/fbs/diplomail/SentResponse.go | 75 +++ ui/Makefile | 2 +- ui/frontend/src/proto/galaxy/fbs/diplomail.ts | 23 + .../galaxy/fbs/diplomail/admin-request.ts | 179 ++++++++ .../galaxy/fbs/diplomail/admin-response.ts | 88 ++++ .../galaxy/fbs/diplomail/broadcast-request.ts | 111 +++++ .../fbs/diplomail/broadcast-response.ts | 77 ++++ .../galaxy/fbs/diplomail/delete-request.ts | 86 ++++ .../galaxy/fbs/diplomail/delete-response.ts | 77 ++++ .../galaxy/fbs/diplomail/inbox-request.ts | 76 ++++ .../galaxy/fbs/diplomail/inbox-response.ts | 94 ++++ .../fbs/diplomail/mail-broadcast-receipt.ts | 242 ++++++++++ .../galaxy/fbs/diplomail/mail-message.ts | 426 ++++++++++++++++++ .../fbs/diplomail/mail-recipient-state.ts | 106 +++++ .../fbs/diplomail/message-get-request.ts | 86 ++++ .../fbs/diplomail/message-get-response.ts | 77 ++++ .../galaxy/fbs/diplomail/read-request.ts | 86 ++++ .../galaxy/fbs/diplomail/read-response.ts | 77 ++++ .../galaxy/fbs/diplomail/send-request.ts | 145 ++++++ .../galaxy/fbs/diplomail/send-response.ts | 77 ++++ .../galaxy/fbs/diplomail/sent-request.ts | 76 ++++ .../galaxy/fbs/diplomail/sent-response.ts | 94 ++++ 42 files changed, 4399 insertions(+), 1 deletion(-) create mode 100644 pkg/model/diplomail/diplomail.go create mode 100644 pkg/schema/fbs/diplomail.fbs create mode 100644 pkg/schema/fbs/diplomail/AdminRequest.go create mode 100644 pkg/schema/fbs/diplomail/AdminResponse.go create mode 100644 pkg/schema/fbs/diplomail/BroadcastRequest.go create mode 100644 pkg/schema/fbs/diplomail/BroadcastResponse.go create mode 100644 pkg/schema/fbs/diplomail/DeleteRequest.go create mode 100644 pkg/schema/fbs/diplomail/DeleteResponse.go create mode 100644 pkg/schema/fbs/diplomail/InboxRequest.go create mode 100644 pkg/schema/fbs/diplomail/InboxResponse.go create mode 100644 pkg/schema/fbs/diplomail/MailBroadcastReceipt.go create mode 100644 pkg/schema/fbs/diplomail/MailMessage.go create mode 100644 pkg/schema/fbs/diplomail/MailRecipientState.go create mode 100644 pkg/schema/fbs/diplomail/MessageGetRequest.go create mode 100644 pkg/schema/fbs/diplomail/MessageGetResponse.go create mode 100644 pkg/schema/fbs/diplomail/ReadRequest.go create mode 100644 pkg/schema/fbs/diplomail/ReadResponse.go create mode 100644 pkg/schema/fbs/diplomail/SendRequest.go create mode 100644 pkg/schema/fbs/diplomail/SendResponse.go create mode 100644 pkg/schema/fbs/diplomail/SentRequest.go create mode 100644 pkg/schema/fbs/diplomail/SentResponse.go create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/admin-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/admin-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/delete-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/delete-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/mail-broadcast-receipt.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/mail-message.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/mail-recipient-state.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/read-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/read-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/send-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/send-response.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/sent-request.ts create mode 100644 ui/frontend/src/proto/galaxy/fbs/diplomail/sent-response.ts diff --git a/pkg/model/diplomail/diplomail.go b/pkg/model/diplomail/diplomail.go new file mode 100644 index 0000000..b8f963d --- /dev/null +++ b/pkg/model/diplomail/diplomail.go @@ -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" +) diff --git a/pkg/schema/fbs/diplomail.fbs b/pkg/schema/fbs/diplomail.fbs new file mode 100644 index 0000000..3049612 --- /dev/null +++ b/pkg/schema/fbs/diplomail.fbs @@ -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; +} diff --git a/pkg/schema/fbs/diplomail/AdminRequest.go b/pkg/schema/fbs/diplomail/AdminRequest.go new file mode 100644 index 0000000..b8aa774 --- /dev/null +++ b/pkg/schema/fbs/diplomail/AdminRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/AdminResponse.go b/pkg/schema/fbs/diplomail/AdminResponse.go new file mode 100644 index 0000000..497b708 --- /dev/null +++ b/pkg/schema/fbs/diplomail/AdminResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/BroadcastRequest.go b/pkg/schema/fbs/diplomail/BroadcastRequest.go new file mode 100644 index 0000000..4869364 --- /dev/null +++ b/pkg/schema/fbs/diplomail/BroadcastRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/BroadcastResponse.go b/pkg/schema/fbs/diplomail/BroadcastResponse.go new file mode 100644 index 0000000..9145c15 --- /dev/null +++ b/pkg/schema/fbs/diplomail/BroadcastResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/DeleteRequest.go b/pkg/schema/fbs/diplomail/DeleteRequest.go new file mode 100644 index 0000000..8fc1d85 --- /dev/null +++ b/pkg/schema/fbs/diplomail/DeleteRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/DeleteResponse.go b/pkg/schema/fbs/diplomail/DeleteResponse.go new file mode 100644 index 0000000..ac9fcae --- /dev/null +++ b/pkg/schema/fbs/diplomail/DeleteResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/InboxRequest.go b/pkg/schema/fbs/diplomail/InboxRequest.go new file mode 100644 index 0000000..89ddbf6 --- /dev/null +++ b/pkg/schema/fbs/diplomail/InboxRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/InboxResponse.go b/pkg/schema/fbs/diplomail/InboxResponse.go new file mode 100644 index 0000000..be0305c --- /dev/null +++ b/pkg/schema/fbs/diplomail/InboxResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/MailBroadcastReceipt.go b/pkg/schema/fbs/diplomail/MailBroadcastReceipt.go new file mode 100644 index 0000000..651ca5d --- /dev/null +++ b/pkg/schema/fbs/diplomail/MailBroadcastReceipt.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/MailMessage.go b/pkg/schema/fbs/diplomail/MailMessage.go new file mode 100644 index 0000000..1d278b4 --- /dev/null +++ b/pkg/schema/fbs/diplomail/MailMessage.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/MailRecipientState.go b/pkg/schema/fbs/diplomail/MailRecipientState.go new file mode 100644 index 0000000..fa697d2 --- /dev/null +++ b/pkg/schema/fbs/diplomail/MailRecipientState.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/MessageGetRequest.go b/pkg/schema/fbs/diplomail/MessageGetRequest.go new file mode 100644 index 0000000..b254b05 --- /dev/null +++ b/pkg/schema/fbs/diplomail/MessageGetRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/MessageGetResponse.go b/pkg/schema/fbs/diplomail/MessageGetResponse.go new file mode 100644 index 0000000..985aeff --- /dev/null +++ b/pkg/schema/fbs/diplomail/MessageGetResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/ReadRequest.go b/pkg/schema/fbs/diplomail/ReadRequest.go new file mode 100644 index 0000000..c681a40 --- /dev/null +++ b/pkg/schema/fbs/diplomail/ReadRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/ReadResponse.go b/pkg/schema/fbs/diplomail/ReadResponse.go new file mode 100644 index 0000000..f6abec3 --- /dev/null +++ b/pkg/schema/fbs/diplomail/ReadResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/SendRequest.go b/pkg/schema/fbs/diplomail/SendRequest.go new file mode 100644 index 0000000..b4038cc --- /dev/null +++ b/pkg/schema/fbs/diplomail/SendRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/SendResponse.go b/pkg/schema/fbs/diplomail/SendResponse.go new file mode 100644 index 0000000..7a94850 --- /dev/null +++ b/pkg/schema/fbs/diplomail/SendResponse.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/SentRequest.go b/pkg/schema/fbs/diplomail/SentRequest.go new file mode 100644 index 0000000..551ed66 --- /dev/null +++ b/pkg/schema/fbs/diplomail/SentRequest.go @@ -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() +} diff --git a/pkg/schema/fbs/diplomail/SentResponse.go b/pkg/schema/fbs/diplomail/SentResponse.go new file mode 100644 index 0000000..1dc4354 --- /dev/null +++ b/pkg/schema/fbs/diplomail/SentResponse.go @@ -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() +} diff --git a/ui/Makefile b/ui/Makefile index 425ad12..e55aa65 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm WASM_EXEC := frontend/static/wasm_exec.js TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null) FBS_OUT := frontend/src/proto/galaxy/fbs -FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs +FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs ../pkg/schema/fbs/diplomail.fbs help: @echo "ui targets:" diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail.ts new file mode 100644 index 0000000..2c1912a --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail.ts @@ -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'; diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-request.ts new file mode 100644 index 0000000..8cec6a8 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-request.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-response.ts new file mode 100644 index 0000000..25ac332 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/admin-response.ts @@ -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 { + 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); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-request.ts new file mode 100644 index 0000000..5f86618 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-request.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-response.ts new file mode 100644 index 0000000..55be283 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/broadcast-response.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-request.ts new file mode 100644 index 0000000..0cdf3e1 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-request.ts @@ -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 { + 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); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-response.ts new file mode 100644 index 0000000..8ecc298 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/delete-response.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-request.ts new file mode 100644 index 0000000..c085b5f --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-request.ts @@ -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 { + 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) + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-response.ts new file mode 100644 index 0000000..80d1e2b --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/inbox-response.ts @@ -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 { + 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(this.items.bind(this), this.itemsLength()) + ); +} + + +unpackTo(_o: InboxResponseT): void { + _o.items = this.bb!.createObjList(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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-broadcast-receipt.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-broadcast-receipt.ts new file mode 100644 index 0000000..2e62878 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-broadcast-receipt.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-message.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-message.ts new file mode 100644 index 0000000..7421512 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-message.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-recipient-state.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-recipient-state.ts new file mode 100644 index 0000000..1d5e418 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/mail-recipient-state.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-request.ts new file mode 100644 index 0000000..101d1e7 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-request.ts @@ -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 { + 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); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-response.ts new file mode 100644 index 0000000..da98abf --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/message-get-response.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/read-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/read-request.ts new file mode 100644 index 0000000..cb85754 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/read-request.ts @@ -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 { + 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); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/read-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/read-response.ts new file mode 100644 index 0000000..22e9ab5 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/read-response.ts @@ -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 { + 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 + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/send-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/send-request.ts new file mode 100644 index 0000000..90f0a4b --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/send-request.ts @@ -0,0 +1,145 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { UUID, UUIDT } from '../common/uuid.js'; + + +export class SendRequest implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SendRequest { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest { + return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +gameId(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +recipientUserId():string|null +recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +recipientUserId(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +recipientRaceName():string|null +recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +recipientRaceName(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 8); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +subject():string|null +subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +subject(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 10); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +body():string|null +body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +body(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 12); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startSendRequest(builder:flatbuffers.Builder) { + builder.startObject(5); +} + +static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) { + builder.addFieldStruct(0, gameIdOffset, 0); +} + +static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, recipientUserIdOffset, 0); +} + +static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) { + builder.addFieldOffset(2, recipientRaceNameOffset, 0); +} + +static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) { + builder.addFieldOffset(3, subjectOffset, 0); +} + +static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) { + builder.addFieldOffset(4, bodyOffset, 0); +} + +static endSendRequest(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + builder.requiredField(offset, 4) // game_id + return offset; +} + +static createSendRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, recipientUserIdOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset { + SendRequest.startSendRequest(builder); + SendRequest.addGameId(builder, gameIdOffset); + SendRequest.addRecipientUserId(builder, recipientUserIdOffset); + SendRequest.addRecipientRaceName(builder, recipientRaceNameOffset); + SendRequest.addSubject(builder, subjectOffset); + SendRequest.addBody(builder, bodyOffset); + return SendRequest.endSendRequest(builder); +} + +unpack(): SendRequestT { + return new SendRequestT( + (this.gameId() !== null ? this.gameId()!.unpack() : null), + this.recipientUserId(), + this.recipientRaceName(), + this.subject(), + this.body() + ); +} + + +unpackTo(_o: SendRequestT): void { + _o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null); + _o.recipientUserId = this.recipientUserId(); + _o.recipientRaceName = this.recipientRaceName(); + _o.subject = this.subject(); + _o.body = this.body(); +} +} + +export class SendRequestT implements flatbuffers.IGeneratedObject { +constructor( + public gameId: UUIDT|null = null, + public recipientUserId: string|Uint8Array|null = null, + public recipientRaceName: string|Uint8Array|null = null, + public subject: string|Uint8Array|null = null, + public body: string|Uint8Array|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0); + const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0); + const subject = (this.subject !== null ? builder.createString(this.subject!) : 0); + const body = (this.body !== null ? builder.createString(this.body!) : 0); + + return SendRequest.createSendRequest(builder, + (this.gameId !== null ? this.gameId!.pack(builder) : 0), + recipientUserId, + recipientRaceName, + subject, + body + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/send-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/send-response.ts new file mode 100644 index 0000000..0812be2 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/send-response.ts @@ -0,0 +1,77 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { MailMessage, MailMessageT } from '../diplomail/mail-message.js'; + + +export class SendResponse implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SendResponse { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse { + return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +message(obj?:MailMessage):MailMessage|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null; +} + +static startSendResponse(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, messageOffset, 0); +} + +static endSendResponse(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createSendResponse(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset):flatbuffers.Offset { + SendResponse.startSendResponse(builder); + SendResponse.addMessage(builder, messageOffset); + return SendResponse.endSendResponse(builder); +} + +unpack(): SendResponseT { + return new SendResponseT( + (this.message() !== null ? this.message()!.unpack() : null) + ); +} + + +unpackTo(_o: SendResponseT): void { + _o.message = (this.message() !== null ? this.message()!.unpack() : null); +} +} + +export class SendResponseT implements flatbuffers.IGeneratedObject { +constructor( + public message: MailMessageT|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const message = (this.message !== null ? this.message!.pack(builder) : 0); + + return SendResponse.createSendResponse(builder, + message + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-request.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-request.ts new file mode 100644 index 0000000..c4afe5c --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-request.ts @@ -0,0 +1,76 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { UUID, UUIDT } from '../common/uuid.js'; + + +export class SentRequest implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SentRequest { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest { + return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +gameId(obj?:UUID):UUID|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null; +} + +static startSentRequest(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) { + builder.addFieldStruct(0, gameIdOffset, 0); +} + +static endSentRequest(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + builder.requiredField(offset, 4) // game_id + return offset; +} + +static createSentRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset { + SentRequest.startSentRequest(builder); + SentRequest.addGameId(builder, gameIdOffset); + return SentRequest.endSentRequest(builder); +} + +unpack(): SentRequestT { + return new SentRequestT( + (this.gameId() !== null ? this.gameId()!.unpack() : null) + ); +} + + +unpackTo(_o: SentRequestT): void { + _o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null); +} +} + +export class SentRequestT implements flatbuffers.IGeneratedObject { +constructor( + public gameId: UUIDT|null = null +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + return SentRequest.createSentRequest(builder, + (this.gameId !== null ? this.gameId!.pack(builder) : 0) + ); +} +} diff --git a/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-response.ts b/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-response.ts new file mode 100644 index 0000000..2799c80 --- /dev/null +++ b/ui/frontend/src/proto/galaxy/fbs/diplomail/sent-response.ts @@ -0,0 +1,94 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ + +import * as flatbuffers from 'flatbuffers'; + +import { MailMessage, MailMessageT } from '../diplomail/mail-message.js'; + + +export class SentResponse implements flatbuffers.IUnpackableObject { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):SentResponse { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse { + return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +items(index: number, obj?:MailMessage):MailMessage|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null; +} + +itemsLength():number { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0; +} + +static startSentResponse(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addItems(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, itemsOffset, 0); +} + +static createItemsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset { + builder.startVector(4, data.length, 4); + for (let i = data.length - 1; i >= 0; i--) { + builder.addOffset(data[i]!); + } + return builder.endVector(); +} + +static startItemsVector(builder:flatbuffers.Builder, numElems:number) { + builder.startVector(4, numElems, 4); +} + +static endSentResponse(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createSentResponse(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset):flatbuffers.Offset { + SentResponse.startSentResponse(builder); + SentResponse.addItems(builder, itemsOffset); + return SentResponse.endSentResponse(builder); +} + +unpack(): SentResponseT { + return new SentResponseT( + this.bb!.createObjList(this.items.bind(this), this.itemsLength()) + ); +} + + +unpackTo(_o: SentResponseT): void { + _o.items = this.bb!.createObjList(this.items.bind(this), this.itemsLength()); +} +} + +export class SentResponseT implements flatbuffers.IGeneratedObject { +constructor( + public items: (MailMessageT)[] = [] +){} + + +pack(builder:flatbuffers.Builder): flatbuffers.Offset { + const items = SentResponse.createItemsVector(builder, builder.createObjectOffsetList(this.items)); + + return SentResponse.createSentResponse(builder, + items + ); +} +} -- 2.52.0 From 57d2286f5e2a2ff55b8154ac5ffea4567d4eaddc Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:27:39 +0200 Subject: [PATCH 03/11] Phase 28 (Step 3a): /sent returns full message detail per recipient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- backend/internal/diplomail/service.go | 11 +++-- backend/internal/diplomail/store.go | 37 +++++++++++----- backend/internal/server/handlers_user_mail.go | 43 +++++------------- backend/openapi.yaml | 44 ++++--------------- 4 files changed, 50 insertions(+), 85 deletions(-) diff --git a/backend/internal/diplomail/service.go b/backend/internal/diplomail/service.go index 7c350b5..a79374a 100644 --- a/backend/internal/diplomail/service.go +++ b/backend/internal/diplomail/service.go @@ -316,10 +316,13 @@ func (s *Service) allowedKinds(ctx context.Context, gameID, userID uuid.UUID) (m return map[string]bool{KindAdmin: true}, nil } -// ListSent returns personal messages authored by senderUserID in -// gameID, newest first. Admin/system rows have no `sender_user_id` -// and are therefore excluded; the user surface does not need them. -func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) { +// ListSent returns the sender-side view of personal messages +// authored by senderUserID in gameID, newest first. Each entry pairs +// the message with one of its recipient rows; single sends contribute +// one entry per message, broadcasts contribute one entry per +// addressee. Admin and system rows have no `sender_user_id` and are +// therefore excluded; the user surface does not need them. +func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) { return s.deps.Store.ListSent(ctx, gameID, senderUserID) } diff --git a/backend/internal/diplomail/store.go b/backend/internal/diplomail/store.go index 6d4f5c9..d2394fc 100644 --- a/backend/internal/diplomail/store.go +++ b/backend/internal/diplomail/store.go @@ -243,25 +243,38 @@ func (s *Store) ListInbox(ctx context.Context, gameID, userID uuid.UUID) ([]Inbo return out, nil } -// ListSent returns messages authored by senderUserID in gameID, -// newest first. Personal messages only — admin/system rows have -// `sender_user_id IS NULL` and are filtered out by the WHERE clause. -func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) { +// ListSent returns the sender-side view of personal messages +// authored by senderUserID in gameID, newest first. Each +// `InboxEntry` carries the message together with one of its +// recipient rows — single sends produce one entry per message; +// game broadcasts produce one entry per addressee (the in-game +// mail UI collapses broadcast entries into a single stand-alone +// item by `message_id`). Admin / system rows have +// `sender_user_id IS NULL` and are excluded by the WHERE clause. +func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) { m := table.DiplomailMessages - stmt := postgres.SELECT(messageColumns()). - FROM(m). + r := table.DiplomailRecipients + cols := append(messageColumns(), recipientColumns()...) + stmt := postgres.SELECT(cols). + FROM(m.INNER_JOIN(r, r.MessageID.EQ(m.MessageID))). WHERE( m.GameID.EQ(postgres.UUID(gameID)). AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))), ). - ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC()) - var rows []model.DiplomailMessages - if err := stmt.QueryContext(ctx, s.db, &rows); err != nil { + ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC(), r.RecipientID.ASC()) + var dest []struct { + model.DiplomailMessages + Recipient model.DiplomailRecipients `alias:"diplomail_recipients"` + } + if err := stmt.QueryContext(ctx, s.db, &dest); err != nil { return nil, fmt.Errorf("diplomail store: list sent %s/%s: %w", gameID, senderUserID, err) } - out := make([]Message, 0, len(rows)) - for _, row := range rows { - out = append(out, messageFromModel(row)) + out := make([]InboxEntry, 0, len(dest)) + for _, row := range dest { + out = append(out, InboxEntry{ + Message: messageFromModel(row.DiplomailMessages), + Recipient: recipientFromModel(row.Recipient), + }) } return out, nil } diff --git a/backend/internal/server/handlers_user_mail.go b/backend/internal/server/handlers_user_mail.go index 04fd8f8..7ab053a 100644 --- a/backend/internal/server/handlers_user_mail.go +++ b/backend/internal/server/handlers_user_mail.go @@ -194,9 +194,9 @@ func (h *UserMailHandlers) Sent() gin.HandlerFunc { respondDiplomailError(c, h.logger, "user mail sent", ctx, err) return } - out := userMailSentListWire{Items: make([]userMailSentSummaryWire, 0, len(items))} - for _, m := range items { - out.Items = append(out.Items, mailMessageSummaryToWire(m)) + out := userMailSentListWire{Items: make([]userMailMessageDetailWire, 0, len(items))} + for _, entry := range items { + out.Items = append(out.Items, mailMessageDetailToWire(entry, false)) } c.JSON(http.StatusOK, out) } @@ -555,27 +555,18 @@ type userMailMessageDetailWire struct { Translator *string `json:"translator,omitempty"` } -// userMailSentSummaryWire mirrors the response shape for the -// sender-side listing. Recipient state is intentionally omitted (one -// author may have N recipients per broadcast in later stages). -type userMailSentSummaryWire struct { - MessageID string `json:"message_id"` - GameID string `json:"game_id"` - GameName string `json:"game_name,omitempty"` - Kind string `json:"kind"` - Subject string `json:"subject,omitempty"` - Body string `json:"body"` - BodyLang string `json:"body_lang"` - BroadcastScope string `json:"broadcast_scope"` - CreatedAt string `json:"created_at"` -} - type userMailInboxListWire struct { Items []userMailMessageDetailWire `json:"items"` } +// userMailSentListWire mirrors the response shape for the +// sender-side listing. Phase 28's in-game UI threads sent messages +// by the recipient's race name, so the wire carries the full +// message detail (including the recipient snapshot) — single sends +// contribute one row per message, broadcasts contribute one row per +// addressee and the UI collapses them by `message_id`. type userMailSentListWire struct { - Items []userMailSentSummaryWire `json:"items"` + Items []userMailMessageDetailWire `json:"items"` } type userMailUnreadCountWire struct { @@ -643,20 +634,6 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM return out } -func mailMessageSummaryToWire(m diplomail.Message) userMailSentSummaryWire { - return userMailSentSummaryWire{ - MessageID: m.MessageID.String(), - GameID: m.GameID.String(), - GameName: m.GameName, - Kind: m.Kind, - Subject: m.Subject, - Body: m.Body, - BodyLang: m.BodyLang, - BroadcastScope: m.BroadcastScope, - CreatedAt: m.CreatedAt.UTC().Format(timestampLayout), - } -} - // mailRecipientStateToWire renders the recipient row after a // mark-read or soft-delete call. The caller only needs the per-user // state, not the full message body again. diff --git a/backend/openapi.yaml b/backend/openapi.yaml index ae89e0b..4477cb2 100644 --- a/backend/openapi.yaml +++ b/backend/openapi.yaml @@ -4400,41 +4400,6 @@ components: translator: type: string description: Identifier of the translation engine that produced the cached row. - UserMailSentSummary: - type: object - additionalProperties: false - required: - - message_id - - game_id - - kind - - body - - body_lang - - broadcast_scope - - created_at - properties: - message_id: - type: string - format: uuid - game_id: - type: string - format: uuid - game_name: - type: string - kind: - type: string - enum: [personal, admin] - subject: - type: string - body: - type: string - body_lang: - type: string - broadcast_scope: - type: string - enum: [single, game_broadcast, multi_game_broadcast] - created_at: - type: string - format: date-time UserMailInboxList: type: object additionalProperties: false @@ -4445,6 +4410,13 @@ components: items: $ref: "#/components/schemas/UserMailMessageDetail" UserMailSentList: + description: | + Sender-side listing of personal messages authored by the + caller. Each item carries the same shape as inbox entries + (including the recipient snapshot); single sends contribute + one row per message, broadcasts contribute one row per + addressee so the in-game UI can collapse them by + `message_id` into a single stand-alone item. type: object additionalProperties: false required: [items] @@ -4452,7 +4424,7 @@ components: items: type: array items: - $ref: "#/components/schemas/UserMailSentSummary" + $ref: "#/components/schemas/UserMailMessageDetail" UserMailUnreadCount: type: object additionalProperties: false -- 2.52.0 From 4cb03736dec5980ae942116b1f93abe93000cf07 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:32:50 +0200 Subject: [PATCH 04/11] Phase 28 (Step 3): gateway translators for user.games.mail.* 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) --- gateway/cmd/gateway/main.go | 6 +- .../internal/backendclient/mail_commands.go | 567 ++++++++++++++++++ .../backendclient/mail_commands_test.go | 209 +++++++ gateway/internal/backendclient/routes.go | 31 + 4 files changed, 812 insertions(+), 1 deletion(-) create mode 100644 gateway/internal/backendclient/mail_commands.go create mode 100644 gateway/internal/backendclient/mail_commands_test.go diff --git a/gateway/cmd/gateway/main.go b/gateway/cmd/gateway/main.go index 8d41527..2c1095b 100644 --- a/gateway/cmd/gateway/main.go +++ b/gateway/cmd/gateway/main.go @@ -186,7 +186,8 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo userRoutes := backendclient.UserRoutes(backend.REST()) lobbyRoutes := backendclient.LobbyRoutes(backend.REST()) gameRoutes := backendclient.GameRoutes(backend.REST()) - allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes)) + mailRoutes := backendclient.MailRoutes(backend.REST()) + allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes)+len(mailRoutes)) for k, v := range userRoutes { allRoutes[k] = v } @@ -196,6 +197,9 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo for k, v := range gameRoutes { allRoutes[k] = v } + for k, v := range mailRoutes { + allRoutes[k] = v + } cleanup := func() error { return closeRedisClient() diff --git a/gateway/internal/backendclient/mail_commands.go b/gateway/internal/backendclient/mail_commands.go new file mode 100644 index 0000000..a63e80a --- /dev/null +++ b/gateway/internal/backendclient/mail_commands.go @@ -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() +} diff --git a/gateway/internal/backendclient/mail_commands_test.go b/gateway/internal/backendclient/mail_commands_test.go new file mode 100644 index 0000000..7adfeb5 --- /dev/null +++ b/gateway/internal/backendclient/mail_commands_test.go @@ -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 +} diff --git a/gateway/internal/backendclient/routes.go b/gateway/internal/backendclient/routes.go index 7b4e37a..ecd9e1f 100644 --- a/gateway/internal/backendclient/routes.go +++ b/gateway/internal/backendclient/routes.go @@ -4,6 +4,7 @@ import ( "context" "galaxy/gateway/internal/downstream" + diplomailmodel "galaxy/model/diplomail" lobbymodel "galaxy/model/lobby" ordermodel "galaxy/model/order" reportmodel "galaxy/model/report" @@ -67,6 +68,27 @@ func GameRoutes(client *RESTClient) map[string]downstream.Client { } } +// MailRoutes returns the authenticated `user.games.mail.*` downstream +// routes served by backend's diplomail subsystem. When client is nil +// every route resolves to a dependency-unavailable client so the +// static router still recognises the message types. +func MailRoutes(client *RESTClient) map[string]downstream.Client { + target := downstream.Client(unavailableClient{}) + if client != nil { + target = mailCommandClient{rest: client} + } + return map[string]downstream.Client{ + diplomailmodel.MessageTypeUserGamesMailInbox: target, + diplomailmodel.MessageTypeUserGamesMailSent: target, + diplomailmodel.MessageTypeUserGamesMailMessageGet: target, + diplomailmodel.MessageTypeUserGamesMailSend: target, + diplomailmodel.MessageTypeUserGamesMailBroadcast: target, + diplomailmodel.MessageTypeUserGamesMailAdmin: target, + diplomailmodel.MessageTypeUserGamesMailRead: target, + diplomailmodel.MessageTypeUserGamesMailDelete: target, + } +} + type unavailableClient struct{} func (unavailableClient) ExecuteCommand(context.Context, downstream.AuthenticatedCommand) (downstream.UnaryResult, error) { @@ -97,9 +119,18 @@ func (c gameCommandClient) ExecuteCommand(ctx context.Context, command downstrea return c.rest.ExecuteGameCommand(ctx, command) } +type mailCommandClient struct { + rest *RESTClient +} + +func (c mailCommandClient) ExecuteCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) { + return c.rest.ExecuteMailCommand(ctx, command) +} + var ( _ downstream.Client = unavailableClient{} _ downstream.Client = userCommandClient{} _ downstream.Client = lobbyCommandClient{} _ downstream.Client = gameCommandClient{} + _ downstream.Client = mailCommandClient{} ) -- 2.52.0 From 7378d4c8ed9b6accf7178a6f19fceee31f047461 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:35:21 +0200 Subject: [PATCH 05/11] Phase 28 (Step 4): UI api/diplomail.ts wrappers 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) --- ui/frontend/src/api/diplomail.ts | 421 +++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 ui/frontend/src/api/diplomail.ts diff --git a/ui/frontend/src/api/diplomail.ts b/ui/frontend/src/api/diplomail.ts new file mode 100644 index 0000000..8177f24 --- /dev/null +++ b/ui/frontend/src/api/diplomail.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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)); +} -- 2.52.0 From fdd5fd193db105da696f7b1f82ba7b2b20572b30 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:37:32 +0200 Subject: [PATCH 06/11] Phase 28 (Step 5): MailStore reactive state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/frontend/src/lib/mail-store.svelte.ts | 373 +++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 ui/frontend/src/lib/mail-store.svelte.ts diff --git a/ui/frontend/src/lib/mail-store.svelte.ts b/ui/frontend/src/lib/mail-store.svelte.ts new file mode 100644 index 0000000..6183ce9 --- /dev/null +++ b/ui/frontend/src/lib/mail-store.svelte.ts @@ -0,0 +1,373 @@ +// Phase 28 reactive store for the in-game diplomatic-mail view. Owns +// the inbox / sent listings, the per-race threading projection, the +// unread badge counter, and the imperative compose / mark-read / +// delete actions. The companion API wrappers live in +// `src/api/diplomail.ts`; this store coordinates them with the rest +// of the in-game shell. + +import type { GalaxyClient } from "../api/galaxy-client"; +import type { Cache } from "../platform/store/index"; +import { + deleteMessage, + fetchInbox, + fetchMessage, + fetchSent, + markRead, + sendAdmin, + sendBroadcast, + sendPersonal, + type MailMessage, + type SendAdminArgs, + type SendBroadcastArgs, + type SendPersonalArgs, +} from "../api/diplomail"; + +/** + * MailThread groups personal messages exchanged with a single other + * race into one entry. The local player's outgoing messages live + * alongside incoming messages from the same race so the UI renders a + * chat-style transcript. `unreadCount` counts only incoming messages + * with `readAt === null`. + */ +export interface MailThread { + kind: "thread"; + raceName: string; + messages: MailMessage[]; + unreadCount: number; + latestAt: Date; +} + +/** + * MailStandalone wraps a single message that does not participate in + * a race-thread: system mail, admin notifications, and the caller's + * own paid-tier broadcasts. The UI renders these as read-only items + * in the same list as the per-race threads. + */ +export interface MailStandalone { + kind: "standalone"; + message: MailMessage; + latestAt: Date; +} + +export type MailListEntry = MailThread | MailStandalone; + +const CACHE_NAMESPACE = "diplomail"; + +/** + * MailStore is the reactive surface consumed by the active view, the + * header badge, and the push-event handler. One instance per signed- + * in session is enough — the rune fields are scoped to the current + * game and replaced on every `setGame` call so navigating between + * games stays clean. + */ +export class MailStore { + gameId = $state(""); + status: "idle" | "loading" | "ready" | "error" = $state("idle"); + error: string | null = $state(null); + inbox: MailMessage[] = $state([]); + sent: MailMessage[] = $state([]); + + private client: GalaxyClient | null = null; + private cache: Cache | null = null; + + /** + * entries surfaces the unified list-pane projection: per-race + * threads built from incoming + outgoing personal messages plus + * stand-alone items for system / admin / own-broadcast rows. + * Sorted newest-first by the latest message inside each entry. + */ + entries: MailListEntry[] = $derived.by(() => buildEntries(this.inbox, this.sent)); + + /** + * unreadCount drives the header view-menu badge. Counts only + * incoming personal / admin / system messages with `readAt === null`. + * `read_at` is not surfaced to the user in the UI but still + * drives this counter. + */ + unreadCount = $derived.by(() => this.inbox.reduce((acc, m) => (m.readAt === null ? acc + 1 : acc), 0)); + + /** + * init configures the dependencies and fires the initial fetch. + * Safe to call multiple times — calls after the first one are + * routed to `setGame`. `localUserId` is captured so the threading + * projection can tell outgoing messages from incoming when the + * inbox and sent lists are unified. + */ + async init(opts: { + client: GalaxyClient; + cache: Cache; + gameId: string; + }): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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): Promise { + 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): Promise { + 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): Promise { + 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 { + 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 { + 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(); + const standalones: MailStandalone[] = []; + + for (const m of inbox) { + if (isStandaloneIncoming(m)) { + standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt }); + continue; + } + const race = m.senderRaceName ?? ""; + if (race === "") { + standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt }); + continue; + } + mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/true); + } + + for (const m of sent) { + if (isStandaloneOutgoing(m)) { + standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt }); + continue; + } + const race = m.recipientRaceName ?? ""; + if (race === "") { + standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt }); + continue; + } + mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/false); + } + + // Sort each thread's messages oldest → newest for chat-style + // rendering; the entry list itself sorts newest-first by the + // most-recent message timestamp. + for (const thread of threadsByRace.values()) { + thread.messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + const last = thread.messages[thread.messages.length - 1]; + thread.latestAt = last.createdAt; + } + + const entries: MailListEntry[] = [ + ...Array.from(threadsByRace.values()), + ...standalones, + ]; + entries.sort((a, b) => b.latestAt.getTime() - a.latestAt.getTime()); + return entries; +} + +function mergeIntoThread( + threads: Map, + 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(); -- 2.52.0 From f7300f25a37ec6ec9c0c30701177da7bbf67fdc5 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:43:09 +0200 Subject: [PATCH 07/11] Phase 28 (Steps 6+9): mail active view + i18n keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/frontend/src/lib/active-view/mail.svelte | 204 ++++++++++++- .../src/lib/active-view/mail/compose.svelte | 273 ++++++++++++++++++ .../active-view/mail/system-item-pane.svelte | 106 +++++++ .../src/lib/active-view/mail/system-titles.ts | 30 ++ .../lib/active-view/mail/thread-list.svelte | 130 +++++++++ .../lib/active-view/mail/thread-pane.svelte | 255 ++++++++++++++++ ui/frontend/src/lib/i18n/locales/en.ts | 40 +++ ui/frontend/src/lib/i18n/locales/ru.ts | 40 +++ 8 files changed, 1066 insertions(+), 12 deletions(-) create mode 100644 ui/frontend/src/lib/active-view/mail/compose.svelte create mode 100644 ui/frontend/src/lib/active-view/mail/system-item-pane.svelte create mode 100644 ui/frontend/src/lib/active-view/mail/system-titles.ts create mode 100644 ui/frontend/src/lib/active-view/mail/thread-list.svelte create mode 100644 ui/frontend/src/lib/active-view/mail/thread-pane.svelte diff --git a/ui/frontend/src/lib/active-view/mail.svelte b/ui/frontend/src/lib/active-view/mail.svelte index 2c1bb68..edb6e15 100644 --- a/ui/frontend/src/lib/active-view/mail.svelte +++ b/ui/frontend/src/lib/active-view/mail.svelte @@ -1,27 +1,207 @@ -
-

{i18n.t("game.view.mail")}

-

{i18n.t("game.shell.coming_soon")}

+
+
+

{i18n.t("game.view.mail")}

+ +
+ + {#if mailStore.status === "loading"} +

+ {i18n.t("game.mail.loading")} +

+ {:else if mailStore.status === "error"} +

+ {mailStore.error ?? i18n.t("game.mail.load_failed")} +

+ {:else if entries.length === 0} +

+ {i18n.t("game.mail.empty")} +

+ {:else} +
+
+ +
+
+ + {#if selected === null} +

+ {i18n.t("game.mail.select_thread")} +

+ {:else if selected.kind === "thread"} + + {:else} + + {/if} +
+
+ {/if} + + {#if composeOpen} + (composeOpen = false)} + onSent={(raceName: string | null) => { + composeOpen = false; + if (raceName !== null) { + selectedKey = `thread:${raceName}`; + } + }} + /> + {/if}
diff --git a/ui/frontend/src/lib/active-view/mail/compose.svelte b/ui/frontend/src/lib/active-view/mail/compose.svelte new file mode 100644 index 0000000..3cdb3b5 --- /dev/null +++ b/ui/frontend/src/lib/active-view/mail/compose.svelte @@ -0,0 +1,273 @@ + + + +
+
+
+

{i18n.t("game.mail.compose_action")}

+ +
+ + + + {#if kind === "admin"} + + {#if adminTarget === "all"} + + {/if} + {/if} + + {#if kind === "personal" || (kind === "admin" && adminTarget === "user")} + + {/if} + + + + + + {#if error} +

{error}

+ {/if} + +
+ + +
+
+
+ + diff --git a/ui/frontend/src/lib/active-view/mail/system-item-pane.svelte b/ui/frontend/src/lib/active-view/mail/system-item-pane.svelte new file mode 100644 index 0000000..d573bbb --- /dev/null +++ b/ui/frontend/src/lib/active-view/mail/system-item-pane.svelte @@ -0,0 +1,106 @@ + + + +
+

{i18n.t(headerKey)}

+ {#if displaySubject} +
{displaySubject}
+ {/if} +

{displayBody}

+ {#if entry.message.translatedBody} + + {/if} + {#if incoming} + + {/if} +
+ + diff --git a/ui/frontend/src/lib/active-view/mail/system-titles.ts b/ui/frontend/src/lib/active-view/mail/system-titles.ts new file mode 100644 index 0000000..d2954f8 --- /dev/null +++ b/ui/frontend/src/lib/active-view/mail/system-titles.ts @@ -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"; +} diff --git a/ui/frontend/src/lib/active-view/mail/thread-list.svelte b/ui/frontend/src/lib/active-view/mail/thread-list.svelte new file mode 100644 index 0000000..bc933e9 --- /dev/null +++ b/ui/frontend/src/lib/active-view/mail/thread-list.svelte @@ -0,0 +1,130 @@ + + + +
    + {#each entries as entry (entryKey(entry))} +
  • 0} + data-testid="mail-list-row" + > + +
  • + {/each} +
+ + diff --git a/ui/frontend/src/lib/active-view/mail/thread-pane.svelte b/ui/frontend/src/lib/active-view/mail/thread-pane.svelte new file mode 100644 index 0000000..8e3977e --- /dev/null +++ b/ui/frontend/src/lib/active-view/mail/thread-pane.svelte @@ -0,0 +1,255 @@ + + + +
+

{thread.raceName}

+
    + {#each thread.messages as m (m.messageId)} +
  1. +
    + + {#if isOutgoing(m)} + {i18n.t("game.mail.outgoing_label")} + {:else} + {thread.raceName} + {/if} + + +
    + {#if displaySubject(m)} +
    {displaySubject(m)}
    + {/if} +

    {displayBody(m)}

    + {#if m.translatedBody} + + {/if} + {#if !isOutgoing(m)} + + {/if} +
  2. + {/each} +
+ +
+ + + {#if replyError} +

{replyError}

+ {/if} + +
+
+ + diff --git a/ui/frontend/src/lib/i18n/locales/en.ts b/ui/frontend/src/lib/i18n/locales/en.ts index ee65623..be0bca3 100644 --- a/ui/frontend/src/lib/i18n/locales/en.ts +++ b/ui/frontend/src/lib/i18n/locales/en.ts @@ -123,6 +123,46 @@ const en = { "game.view.report": "turn report", "game.view.battle": "battle log", "game.view.mail": "diplomatic mail", + "game.view.mail.badge": "{count}", + "game.events.mail_new.message": "new mail from {from}", + "game.events.mail_new.action": "view", + "game.mail.loading": "loading mail…", + "game.mail.load_failed": "could not load mail", + "game.mail.empty": "no diplomatic messages yet", + "game.mail.back": "back", + "game.mail.compose_action": "compose", + "game.mail.select_thread": "pick a thread on the left to read it", + "game.mail.broadcast.title": "your broadcast", + "game.mail.admin.title": "admin notification", + "game.mail.system.generic.title": "system message", + "game.mail.system.game_paused.title": "game paused", + "game.mail.system.game_cancelled.title": "game cancelled", + "game.mail.system.membership_removed.title": "membership removed", + "game.mail.system.membership_blocked.title": "membership blocked", + "game.mail.subject_placeholder": "subject (optional)", + "game.mail.body_placeholder": "your message…", + "game.mail.recipient_label": "race", + "game.mail.recipient_required": "pick a recipient race", + "game.mail.body_required": "the message body cannot be empty", + "game.mail.body_too_long": "the body exceeds the {limit} byte limit", + "game.mail.subject_too_long": "the subject exceeds the {limit} byte limit", + "game.mail.compose.send": "send", + "game.mail.compose.cancel": "cancel", + "game.mail.compose.target_personal": "personal", + "game.mail.compose.target_broadcast": "broadcast", + "game.mail.compose.target_admin": "admin", + "game.mail.compose.recipients_active": "active members", + "game.mail.compose.recipients_active_and_removed": "active + removed", + "game.mail.compose.recipients_all_members": "all members", + "game.mail.compose.target_label": "kind", + "game.mail.compose.recipients_label": "audience", + "game.mail.compose.send_failed": "send failed", + "game.mail.show_original": "show original", + "game.mail.show_translation": "show translation", + "game.mail.translation_unavailable": "translation unavailable", + "game.mail.reply_label": "reply", + "game.mail.delete_action": "delete", + "game.mail.outgoing_label": "you", "game.view.designer.ship_class": "ship-class designer", "game.view.designer.science": "science designer", "game.sidebar.tab.calculator": "calculator", diff --git a/ui/frontend/src/lib/i18n/locales/ru.ts b/ui/frontend/src/lib/i18n/locales/ru.ts index eafd66c..25dee81 100644 --- a/ui/frontend/src/lib/i18n/locales/ru.ts +++ b/ui/frontend/src/lib/i18n/locales/ru.ts @@ -124,6 +124,46 @@ const ru: Record = { "game.view.report": "отчёт хода", "game.view.battle": "журнал боёв", "game.view.mail": "дипломатическая почта", + "game.view.mail.badge": "{count}", + "game.events.mail_new.message": "новое письмо от {from}", + "game.events.mail_new.action": "открыть", + "game.mail.loading": "загрузка почты…", + "game.mail.load_failed": "не удалось загрузить почту", + "game.mail.empty": "дипломатических сообщений пока нет", + "game.mail.back": "назад", + "game.mail.compose_action": "написать", + "game.mail.select_thread": "выбери ветку слева", + "game.mail.broadcast.title": "твоя рассылка", + "game.mail.admin.title": "административное уведомление", + "game.mail.system.generic.title": "системное сообщение", + "game.mail.system.game_paused.title": "игра поставлена на паузу", + "game.mail.system.game_cancelled.title": "игра отменена", + "game.mail.system.membership_removed.title": "членство удалено", + "game.mail.system.membership_blocked.title": "членство заблокировано", + "game.mail.subject_placeholder": "тема (необязательно)", + "game.mail.body_placeholder": "твоё сообщение…", + "game.mail.recipient_label": "раса", + "game.mail.recipient_required": "выбери расу-получателя", + "game.mail.body_required": "тело сообщения не может быть пустым", + "game.mail.body_too_long": "длина тела превышает лимит {limit} байт", + "game.mail.subject_too_long": "длина темы превышает лимит {limit} байт", + "game.mail.compose.send": "отправить", + "game.mail.compose.cancel": "отмена", + "game.mail.compose.target_personal": "личное", + "game.mail.compose.target_broadcast": "рассылка", + "game.mail.compose.target_admin": "админ.", + "game.mail.compose.recipients_active": "активным членам", + "game.mail.compose.recipients_active_and_removed": "активным + удалённым", + "game.mail.compose.recipients_all_members": "всем членам", + "game.mail.compose.target_label": "тип", + "game.mail.compose.recipients_label": "адресаты", + "game.mail.compose.send_failed": "отправка не удалась", + "game.mail.show_original": "показать оригинал", + "game.mail.show_translation": "показать перевод", + "game.mail.translation_unavailable": "перевод недоступен", + "game.mail.reply_label": "ответить", + "game.mail.delete_action": "удалить", + "game.mail.outgoing_label": "ты", "game.view.designer.ship_class": "конструктор класса кораблей", "game.view.designer.science": "редактор наук", "game.sidebar.tab.calculator": "калькулятор", -- 2.52.0 From db81bd8e08d27fad7dde32f48acc4eb3b252fbbc Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Fri, 15 May 2026 22:46:00 +0200 Subject: [PATCH 08/11] Phase 28 (Steps 7+8): header unread badge + push/init wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ui/frontend/src/lib/header/view-menu.svelte | 26 +++++++++- .../src/routes/games/[id]/+layout.svelte | 52 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/ui/frontend/src/lib/header/view-menu.svelte b/ui/frontend/src/lib/header/view-menu.svelte index cbe5bd4..30f6c99 100644 --- a/ui/frontend/src/lib/header/view-menu.svelte +++ b/ui/frontend/src/lib/header/view-menu.svelte @@ -15,10 +15,13 @@ polishes microcopy. import { onMount } from "svelte"; import { goto } from "$app/navigation"; import { i18n, type TranslationKey } from "$lib/i18n/index.svelte"; + import { mailStore } from "$lib/mail-store.svelte"; type Props = { gameId: string }; let { gameId }: Props = $props(); + const mailUnread = $derived(mailStore.unreadCount); + let open = $state(false); let rootEl: HTMLDivElement | null = $state(null); @@ -122,9 +125,15 @@ polishes microcopy. type="button" role="menuitem" data-testid="view-menu-item-mail" + class="with-badge" onclick={() => go(`/games/${gameId}/mail`)} > - {i18n.t("game.view.mail")} + {i18n.t("game.view.mail")} + {#if mailUnread > 0} + + {i18n.t("game.view.mail.badge", { count: String(mailUnread) })} + + {/if}