Phase 28: diplomatic mail UI (work in progress) #11
@@ -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
|
||||
|
||||
@@ -29,7 +29,11 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp
|
||||
return Message{}, Recipient{}, err
|
||||
}
|
||||
|
||||
recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, in.RecipientUserID)
|
||||
recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName)
|
||||
if err != nil {
|
||||
return Message{}, Recipient{}, err
|
||||
}
|
||||
recipient, err := s.deps.Memberships.GetMembershipAnyStatus(ctx, in.GameID, recipientID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not a member of the game", ErrForbidden)
|
||||
@@ -37,7 +41,7 @@ func (s *Service) SendAdminPersonal(ctx context.Context, in SendAdminPersonalInp
|
||||
return Message{}, Recipient{}, fmt.Errorf("diplomail: load admin recipient: %w", err)
|
||||
}
|
||||
|
||||
msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername,
|
||||
msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername,
|
||||
recipient.GameID, recipient.GameName, subject, body, in.SenderIP, BroadcastScopeSingle)
|
||||
if err != nil {
|
||||
return Message{}, Recipient{}, err
|
||||
@@ -84,7 +88,7 @@ func (s *Service) SendAdminBroadcast(ctx context.Context, in SendAdminBroadcastI
|
||||
}
|
||||
|
||||
gameName := members[0].GameName
|
||||
msgInsert, err := s.buildAdminMessageInsert(in.CallerKind, in.CallerUserID, in.CallerUsername,
|
||||
msgInsert, err := s.buildAdminMessageInsert(ctx, in.CallerKind, in.CallerUserID, in.CallerUsername,
|
||||
in.GameID, gameName, subject, body, in.SenderIP, BroadcastScopeGameBroadcast)
|
||||
if err != nil {
|
||||
return Message{}, nil, err
|
||||
@@ -147,6 +151,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
|
||||
}
|
||||
|
||||
username := sender.UserName
|
||||
senderRace := sender.RaceName
|
||||
msgInsert := MessageInsert{
|
||||
MessageID: uuid.New(),
|
||||
GameID: in.GameID,
|
||||
@@ -155,6 +160,7 @@ func (s *Service) SendPlayerBroadcast(ctx context.Context, in SendPlayerBroadcas
|
||||
SenderKind: SenderKindPlayer,
|
||||
SenderUserID: &callerID,
|
||||
SenderUsername: &username,
|
||||
SenderRaceName: &senderRace,
|
||||
SenderIP: in.SenderIP,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
@@ -217,7 +223,7 @@ func (s *Service) SendAdminMultiGameBroadcast(ctx context.Context, in SendMultiG
|
||||
zap.String("scope", scope))
|
||||
continue
|
||||
}
|
||||
msgInsert, err := s.buildAdminMessageInsert(CallerKindAdmin, nil, in.CallerUsername,
|
||||
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindAdmin, nil, in.CallerUsername,
|
||||
game.GameID, game.GameName, subject, body, in.SenderIP, BroadcastScopeMultiGameBroadcast)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -356,7 +362,7 @@ func (s *Service) publishGameLifecycle(ctx context.Context, ev LifecycleEvent) e
|
||||
gameName := members[0].GameName
|
||||
subject, body := renderGameLifecycle(ev.Kind, gameName, ev.Actor, ev.Reason)
|
||||
|
||||
msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "",
|
||||
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "",
|
||||
ev.GameID, gameName, subject, body, "", BroadcastScopeGameBroadcast)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -385,7 +391,7 @@ func (s *Service) publishMembershipLifecycle(ctx context.Context, ev LifecycleEv
|
||||
}
|
||||
subject, body := renderMembershipLifecycle(ev.Kind, target.GameName, ev.Actor, ev.Reason)
|
||||
|
||||
msgInsert, err := s.buildAdminMessageInsert(CallerKindSystem, nil, "",
|
||||
msgInsert, err := s.buildAdminMessageInsert(ctx, CallerKindSystem, nil, "",
|
||||
ev.GameID, target.GameName, subject, body, "", BroadcastScopeSingle)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -417,10 +423,12 @@ func (s *Service) prepareContent(subject, body string) (string, string, error) {
|
||||
// for every admin-kind send. The CHECK constraint maps sender
|
||||
// shapes:
|
||||
//
|
||||
// sender_kind='player' → CallerKind owner; sender_user_id set
|
||||
// sender_kind='player' → CallerKind owner; sender_user_id set,
|
||||
// sender_race_name resolved from
|
||||
// Memberships.GetActiveMembership
|
||||
// sender_kind='admin' → CallerKind admin; sender_user_id nil
|
||||
// sender_kind='system' → CallerKind system; sender_username nil
|
||||
func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.UUID, callerUsername string,
|
||||
func (s *Service) buildAdminMessageInsert(ctx context.Context, callerKind string, callerUserID *uuid.UUID, callerUsername string,
|
||||
gameID uuid.UUID, gameName, subject, body, senderIP, scope string) (MessageInsert, error) {
|
||||
out := MessageInsert{
|
||||
MessageID: uuid.New(),
|
||||
@@ -443,6 +451,17 @@ func (s *Service) buildAdminMessageInsert(callerKind string, callerUserID *uuid.
|
||||
out.SenderKind = SenderKindPlayer
|
||||
out.SenderUserID = &uid
|
||||
out.SenderUsername = &uname
|
||||
// Owner race snapshot is best-effort: a private-game owner who
|
||||
// has an active membership in their own game contributes a
|
||||
// race name; an owner who is not a current member (or whose
|
||||
// membership is removed/blocked) leaves the field nil. The
|
||||
// CHECK constraint accepts both shapes for sender_kind='player'.
|
||||
if ownerMember, err := s.deps.Memberships.GetActiveMembership(ctx, gameID, uid); err == nil {
|
||||
race := ownerMember.RaceName
|
||||
out.SenderRaceName = &race
|
||||
} else if !errors.Is(err, ErrNotFound) {
|
||||
return MessageInsert{}, fmt.Errorf("diplomail: load owner membership: %w", err)
|
||||
}
|
||||
case CallerKindAdmin:
|
||||
uname := callerUsername
|
||||
out.SenderKind = SenderKindAdmin
|
||||
|
||||
@@ -369,6 +369,114 @@ func TestDiplomailPersonalFlow(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiplomailPersonalByRaceName exercises the Phase 28 contract: the
|
||||
// UI passes a recipient race name (read out of the game report); the
|
||||
// service resolves it to the active member with that race name and
|
||||
// snapshots the sender's race onto the message row. Error cases cover
|
||||
// the validation rules baked into the wire schema.
|
||||
func TestDiplomailPersonalByRaceName(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
ctx := context.Background()
|
||||
|
||||
gameID := uuid.New()
|
||||
sender := uuid.New()
|
||||
recipient := uuid.New()
|
||||
kicked := uuid.New()
|
||||
seedAccount(t, db, sender)
|
||||
seedAccount(t, db, recipient)
|
||||
seedAccount(t, db, kicked)
|
||||
seedGame(t, db, gameID, "Race-Name Resolution Game")
|
||||
|
||||
lookup := &staticMembershipLookup{
|
||||
rows: map[[2]uuid.UUID]diplomail.ActiveMembership{
|
||||
{gameID, sender}: {
|
||||
UserID: sender, GameID: gameID, GameName: "Race-Name Resolution Game",
|
||||
UserName: "sender", RaceName: "Senders",
|
||||
},
|
||||
{gameID, recipient}: {
|
||||
UserID: recipient, GameID: gameID, GameName: "Race-Name Resolution Game",
|
||||
UserName: "recipient", RaceName: "Receivers",
|
||||
},
|
||||
},
|
||||
inactive: map[[2]uuid.UUID]diplomail.MemberSnapshot{
|
||||
{gameID, kicked}: {
|
||||
UserID: kicked, GameID: gameID, GameName: "Race-Name Resolution Game",
|
||||
UserName: "kicked", RaceName: "Departed", Status: "removed",
|
||||
},
|
||||
},
|
||||
}
|
||||
svc := diplomail.NewService(diplomail.Deps{
|
||||
Store: diplomail.NewStore(db),
|
||||
Memberships: lookup,
|
||||
Notification: &recordingPublisher{},
|
||||
Config: config.DiplomailConfig{
|
||||
MaxBodyBytes: 4096,
|
||||
MaxSubjectBytes: 256,
|
||||
},
|
||||
})
|
||||
|
||||
// Happy path: race name resolves and sender_race_name is snapshotted.
|
||||
msg, rcpt, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: sender,
|
||||
RecipientRaceName: "Receivers",
|
||||
Subject: "Trade",
|
||||
Body: "Care to talk?",
|
||||
SenderIP: "203.0.113.4",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send by race name: %v", err)
|
||||
}
|
||||
if rcpt.UserID != recipient {
|
||||
t.Fatalf("recipient = %s, want %s", rcpt.UserID, recipient)
|
||||
}
|
||||
if msg.SenderRaceName == nil || *msg.SenderRaceName != "Senders" {
|
||||
t.Fatalf("sender_race_name = %v, want \"Senders\"", msg.SenderRaceName)
|
||||
}
|
||||
|
||||
// Both identifiers supplied → invalid_request.
|
||||
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: sender,
|
||||
RecipientUserID: recipient,
|
||||
RecipientRaceName: "Receivers",
|
||||
Body: "x",
|
||||
}); !errors.Is(err, diplomail.ErrInvalidInput) {
|
||||
t.Fatalf("dual identifier: %v, want ErrInvalidInput", err)
|
||||
}
|
||||
|
||||
// Neither identifier supplied → invalid_request.
|
||||
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: sender,
|
||||
Body: "x",
|
||||
}); !errors.Is(err, diplomail.ErrInvalidInput) {
|
||||
t.Fatalf("no identifier: %v, want ErrInvalidInput", err)
|
||||
}
|
||||
|
||||
// Race name with no matching active member → invalid_request.
|
||||
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: sender,
|
||||
RecipientRaceName: "Strangers",
|
||||
Body: "x",
|
||||
}); !errors.Is(err, diplomail.ErrInvalidInput) {
|
||||
t.Fatalf("unknown race: %v, want ErrInvalidInput", err)
|
||||
}
|
||||
|
||||
// Race name of a lobby-removed member → invalid_request (the
|
||||
// active-only scope filters them out; the lookup never returns
|
||||
// them).
|
||||
if _, _, err := svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: sender,
|
||||
RecipientRaceName: "Departed",
|
||||
Body: "x",
|
||||
}); !errors.Is(err, diplomail.ErrInvalidInput) {
|
||||
t.Fatalf("kicked race: %v, want ErrInvalidInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiplomailRejectsNonActiveSender(t *testing.T) {
|
||||
db := startPostgres(t)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -32,15 +32,20 @@ const previewMaxRunes = 120
|
||||
// ErrForbidden; the inserted Message is never persisted in those
|
||||
// cases.
|
||||
func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Message, Recipient, error) {
|
||||
if in.SenderUserID == in.RecipientUserID {
|
||||
return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput)
|
||||
}
|
||||
subject := strings.TrimRight(in.Subject, " \t")
|
||||
body := strings.TrimRight(in.Body, " \t\n")
|
||||
if err := s.validateContent(subject, body); err != nil {
|
||||
return Message{}, Recipient{}, err
|
||||
}
|
||||
|
||||
recipientID, err := s.resolveActiveRecipient(ctx, in.GameID, in.RecipientUserID, in.RecipientRaceName)
|
||||
if err != nil {
|
||||
return Message{}, Recipient{}, err
|
||||
}
|
||||
if in.SenderUserID == recipientID {
|
||||
return Message{}, Recipient{}, fmt.Errorf("%w: cannot send mail to yourself", ErrInvalidInput)
|
||||
}
|
||||
|
||||
sender, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.SenderUserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
@@ -48,7 +53,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
|
||||
}
|
||||
return Message{}, Recipient{}, fmt.Errorf("diplomail: load sender membership: %w", err)
|
||||
}
|
||||
recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, in.RecipientUserID)
|
||||
recipient, err := s.deps.Memberships.GetActiveMembership(ctx, in.GameID, recipientID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return Message{}, Recipient{}, fmt.Errorf("%w: recipient is not an active member of the game", ErrForbidden)
|
||||
@@ -57,14 +62,17 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
|
||||
}
|
||||
|
||||
username := sender.UserName
|
||||
senderRace := sender.RaceName
|
||||
senderUserID := in.SenderUserID
|
||||
msgInsert := MessageInsert{
|
||||
MessageID: uuid.New(),
|
||||
GameID: in.GameID,
|
||||
GameName: sender.GameName,
|
||||
Kind: KindPersonal,
|
||||
SenderKind: SenderKindPlayer,
|
||||
SenderUserID: &in.SenderUserID,
|
||||
SenderUserID: &senderUserID,
|
||||
SenderUsername: &username,
|
||||
SenderRaceName: &senderRace,
|
||||
SenderIP: in.SenderIP,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
@@ -75,7 +83,7 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
|
||||
rcptInsert := buildRecipientInsert(
|
||||
msgInsert.MessageID,
|
||||
MemberSnapshot{
|
||||
UserID: in.RecipientUserID,
|
||||
UserID: recipientID,
|
||||
GameID: in.GameID,
|
||||
GameName: recipient.GameName,
|
||||
UserName: recipient.UserName,
|
||||
@@ -101,6 +109,47 @@ func (s *Service) SendPersonal(ctx context.Context, in SendPersonalInput) (Messa
|
||||
return msg, recipients[0], nil
|
||||
}
|
||||
|
||||
// resolveActiveRecipient turns a (user_id, race_name) pair into the
|
||||
// canonical user id of an active member of gameID. Exactly one of the
|
||||
// two inputs must be set; both-set or both-empty returns
|
||||
// ErrInvalidInput. Race-name resolution is restricted to the active
|
||||
// scope so lobby-removed and blocked members cannot be reached
|
||||
// through the race-name shortcut. ErrInvalidInput is also returned
|
||||
// when the race name matches zero members; ErrForbidden when the
|
||||
// race name matches more than one active row (defence in depth — race
|
||||
// names are unique within a game by lobby invariant).
|
||||
func (s *Service) resolveActiveRecipient(ctx context.Context, gameID uuid.UUID, byUserID uuid.UUID, byRaceName string) (uuid.UUID, error) {
|
||||
byRaceName = strings.TrimSpace(byRaceName)
|
||||
hasUser := byUserID != uuid.Nil
|
||||
hasRace := byRaceName != ""
|
||||
switch {
|
||||
case hasUser && hasRace:
|
||||
return uuid.Nil, fmt.Errorf("%w: only one of recipient_user_id, recipient_race_name may be supplied", ErrInvalidInput)
|
||||
case !hasUser && !hasRace:
|
||||
return uuid.Nil, fmt.Errorf("%w: recipient_user_id or recipient_race_name must be supplied", ErrInvalidInput)
|
||||
case hasUser:
|
||||
return byUserID, nil
|
||||
}
|
||||
members, err := s.deps.Memberships.ListMembers(ctx, gameID, RecipientScopeActive)
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("diplomail: list active members for race lookup: %w", err)
|
||||
}
|
||||
var found []MemberSnapshot
|
||||
for _, m := range members {
|
||||
if m.RaceName == byRaceName {
|
||||
found = append(found, m)
|
||||
}
|
||||
}
|
||||
switch len(found) {
|
||||
case 0:
|
||||
return uuid.Nil, fmt.Errorf("%w: no active member with race %q in this game", ErrInvalidInput, byRaceName)
|
||||
case 1:
|
||||
return found[0].UserID, nil
|
||||
default:
|
||||
return uuid.Nil, fmt.Errorf("%w: race %q matches multiple active members", ErrForbidden, byRaceName)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMessage returns the InboxEntry for messageID addressed to
|
||||
// userID. ErrNotFound is returned when the caller is not a recipient
|
||||
// of the message — handlers translate that to 404 so the existence
|
||||
@@ -267,10 +316,13 @@ func (s *Service) allowedKinds(ctx context.Context, gameID, userID uuid.UUID) (m
|
||||
return map[string]bool{KindAdmin: true}, nil
|
||||
}
|
||||
|
||||
// ListSent returns personal messages authored by senderUserID in
|
||||
// gameID, newest first. Admin/system rows have no `sender_user_id`
|
||||
// and are therefore excluded; the user surface does not need them.
|
||||
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
|
||||
// ListSent returns the sender-side view of personal messages
|
||||
// authored by senderUserID in gameID, newest first. Each entry pairs
|
||||
// the message with one of its recipient rows; single sends contribute
|
||||
// one entry per message, broadcasts contribute one entry per
|
||||
// addressee. Admin and system rows have no `sender_user_id` and are
|
||||
// therefore excluded; the user surface does not need them.
|
||||
func (s *Service) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
|
||||
return s.deps.Store.ListSent(ctx, gameID, senderUserID)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func messageColumns() postgres.ColumnList {
|
||||
m := table.DiplomailMessages
|
||||
return postgres.ColumnList{
|
||||
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind,
|
||||
m.SenderUserID, m.SenderUsername, m.SenderIP,
|
||||
m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP,
|
||||
m.Subject, m.Body, m.BodyLang, m.BroadcastScope, m.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ type MessageInsert struct {
|
||||
SenderKind string
|
||||
SenderUserID *uuid.UUID
|
||||
SenderUsername *string
|
||||
SenderRaceName *string
|
||||
SenderIP string
|
||||
Subject string
|
||||
Body string
|
||||
@@ -101,7 +102,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
|
||||
m := table.DiplomailMessages
|
||||
msgStmt := m.INSERT(
|
||||
m.MessageID, m.GameID, m.GameName, m.Kind, m.SenderKind,
|
||||
m.SenderUserID, m.SenderUsername, m.SenderIP,
|
||||
m.SenderUserID, m.SenderUsername, m.SenderRaceName, m.SenderIP,
|
||||
m.Subject, m.Body, m.BodyLang, m.BroadcastScope,
|
||||
).VALUES(
|
||||
msg.MessageID,
|
||||
@@ -111,6 +112,7 @@ func (s *Store) InsertMessageWithRecipients(ctx context.Context, msg MessageInse
|
||||
msg.SenderKind,
|
||||
uuidPtrArg(msg.SenderUserID),
|
||||
stringPtrArg(msg.SenderUsername),
|
||||
stringPtrArg(msg.SenderRaceName),
|
||||
msg.SenderIP,
|
||||
msg.Subject,
|
||||
msg.Body,
|
||||
@@ -241,25 +243,38 @@ func (s *Store) ListInbox(ctx context.Context, gameID, userID uuid.UUID) ([]Inbo
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListSent returns messages authored by senderUserID in gameID,
|
||||
// newest first. Personal messages only — admin/system rows have
|
||||
// `sender_user_id IS NULL` and are filtered out by the WHERE clause.
|
||||
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]Message, error) {
|
||||
// ListSent returns the sender-side view of personal messages
|
||||
// authored by senderUserID in gameID, newest first. Each
|
||||
// `InboxEntry` carries the message together with one of its
|
||||
// recipient rows — single sends produce one entry per message;
|
||||
// game broadcasts produce one entry per addressee (the in-game
|
||||
// mail UI collapses broadcast entries into a single stand-alone
|
||||
// item by `message_id`). Admin / system rows have
|
||||
// `sender_user_id IS NULL` and are excluded by the WHERE clause.
|
||||
func (s *Store) ListSent(ctx context.Context, gameID, senderUserID uuid.UUID) ([]InboxEntry, error) {
|
||||
m := table.DiplomailMessages
|
||||
stmt := postgres.SELECT(messageColumns()).
|
||||
FROM(m).
|
||||
r := table.DiplomailRecipients
|
||||
cols := append(messageColumns(), recipientColumns()...)
|
||||
stmt := postgres.SELECT(cols).
|
||||
FROM(m.INNER_JOIN(r, r.MessageID.EQ(m.MessageID))).
|
||||
WHERE(
|
||||
m.GameID.EQ(postgres.UUID(gameID)).
|
||||
AND(m.SenderUserID.EQ(postgres.UUID(senderUserID))),
|
||||
).
|
||||
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC())
|
||||
var rows []model.DiplomailMessages
|
||||
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
|
||||
ORDER_BY(m.CreatedAt.DESC(), m.MessageID.DESC(), r.RecipientID.ASC())
|
||||
var dest []struct {
|
||||
model.DiplomailMessages
|
||||
Recipient model.DiplomailRecipients `alias:"diplomail_recipients"`
|
||||
}
|
||||
if err := stmt.QueryContext(ctx, s.db, &dest); err != nil {
|
||||
return nil, fmt.Errorf("diplomail store: list sent %s/%s: %w", gameID, senderUserID, err)
|
||||
}
|
||||
out := make([]Message, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, messageFromModel(row))
|
||||
out := make([]InboxEntry, 0, len(dest))
|
||||
for _, row := range dest {
|
||||
out = append(out, InboxEntry{
|
||||
Message: messageFromModel(row.DiplomailMessages),
|
||||
Recipient: recipientFromModel(row.Recipient),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -737,6 +752,10 @@ func messageFromModel(row model.DiplomailMessages) Message {
|
||||
name := *row.SenderUsername
|
||||
out.SenderUsername = &name
|
||||
}
|
||||
if row.SenderRaceName != nil {
|
||||
name := *row.SenderRaceName
|
||||
out.SenderRaceName = &name
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ type Message struct {
|
||||
SenderKind string
|
||||
SenderUserID *uuid.UUID
|
||||
SenderUsername *string
|
||||
// SenderRaceName carries the snapshot of the sender's race in the
|
||||
// game at send time. Non-nil for sender_kind='player' rows, nil
|
||||
// for admin and system. The in-game mail UI groups personal
|
||||
// threads by this name (Phase 28).
|
||||
SenderRaceName *string
|
||||
SenderIP string
|
||||
Subject string
|
||||
Body string
|
||||
@@ -92,16 +97,19 @@ type Translation struct {
|
||||
}
|
||||
|
||||
// SendPersonalInput is the request payload for SendPersonal: the
|
||||
// caller sending a single-recipient personal message. Validation
|
||||
// (active membership, body length, etc.) is performed inside the
|
||||
// service.
|
||||
// caller sending a single-recipient personal message. Exactly one of
|
||||
// RecipientUserID and RecipientRaceName must be non-zero; the
|
||||
// service resolves a non-empty RecipientRaceName to the active
|
||||
// member with that race in the game. Other validation (active
|
||||
// membership, body length, etc.) is performed inside the service.
|
||||
type SendPersonalInput struct {
|
||||
GameID uuid.UUID
|
||||
SenderUserID uuid.UUID
|
||||
RecipientUserID uuid.UUID
|
||||
Subject string
|
||||
Body string
|
||||
SenderIP string
|
||||
GameID uuid.UUID
|
||||
SenderUserID uuid.UUID
|
||||
RecipientUserID uuid.UUID
|
||||
RecipientRaceName string
|
||||
Subject string
|
||||
Body string
|
||||
SenderIP string
|
||||
}
|
||||
|
||||
// CallerKind enumerates the privileged sender roles for admin-kind
|
||||
@@ -116,17 +124,21 @@ const (
|
||||
|
||||
// SendAdminPersonalInput is the request payload for an owner /
|
||||
// admin / system sending an admin-kind message to a single
|
||||
// recipient. Authorization (owner-vs-admin distinction) is enforced
|
||||
// by the HTTP layer; the service trusts the caller designation.
|
||||
// recipient. Exactly one of RecipientUserID and RecipientRaceName
|
||||
// must be non-zero; the service resolves a non-empty
|
||||
// RecipientRaceName to the active member with that race in the
|
||||
// game. Authorization (owner-vs-admin distinction) is enforced by
|
||||
// the HTTP layer; the service trusts the caller designation.
|
||||
type SendAdminPersonalInput struct {
|
||||
GameID uuid.UUID
|
||||
CallerKind string
|
||||
CallerUserID *uuid.UUID
|
||||
CallerUsername string
|
||||
RecipientUserID uuid.UUID
|
||||
Subject string
|
||||
Body string
|
||||
SenderIP string
|
||||
GameID uuid.UUID
|
||||
CallerKind string
|
||||
CallerUserID *uuid.UUID
|
||||
CallerUsername string
|
||||
RecipientUserID uuid.UUID
|
||||
RecipientRaceName string
|
||||
Subject string
|
||||
Body string
|
||||
SenderIP string
|
||||
}
|
||||
|
||||
// SendAdminBroadcastInput is the request payload for an owner /
|
||||
|
||||
@@ -20,6 +20,7 @@ type DiplomailMessages struct {
|
||||
SenderKind string
|
||||
SenderUserID *uuid.UUID
|
||||
SenderUsername *string
|
||||
SenderRaceName *string
|
||||
SenderIP string
|
||||
Subject string
|
||||
Body string
|
||||
|
||||
@@ -24,6 +24,7 @@ type diplomailMessagesTable struct {
|
||||
SenderKind postgres.ColumnString
|
||||
SenderUserID postgres.ColumnString
|
||||
SenderUsername postgres.ColumnString
|
||||
SenderRaceName postgres.ColumnString
|
||||
SenderIP postgres.ColumnString
|
||||
Subject postgres.ColumnString
|
||||
Body postgres.ColumnString
|
||||
@@ -78,14 +79,15 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
|
||||
SenderKindColumn = postgres.StringColumn("sender_kind")
|
||||
SenderUserIDColumn = postgres.StringColumn("sender_user_id")
|
||||
SenderUsernameColumn = postgres.StringColumn("sender_username")
|
||||
SenderRaceNameColumn = postgres.StringColumn("sender_race_name")
|
||||
SenderIPColumn = postgres.StringColumn("sender_ip")
|
||||
SubjectColumn = postgres.StringColumn("subject")
|
||||
BodyColumn = postgres.StringColumn("body")
|
||||
BodyLangColumn = postgres.StringColumn("body_lang")
|
||||
BroadcastScopeColumn = postgres.StringColumn("broadcast_scope")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
|
||||
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderRaceNameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{SenderIPColumn, SubjectColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
@@ -100,6 +102,7 @@ func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomai
|
||||
SenderKind: SenderKindColumn,
|
||||
SenderUserID: SenderUserIDColumn,
|
||||
SenderUsername: SenderUsernameColumn,
|
||||
SenderRaceName: SenderRaceNameColumn,
|
||||
SenderIP: SenderIPColumn,
|
||||
Subject: SubjectColumn,
|
||||
Body: BodyColumn,
|
||||
|
||||
@@ -683,6 +683,11 @@ CREATE TABLE diplomail_messages (
|
||||
sender_kind text NOT NULL,
|
||||
sender_user_id uuid,
|
||||
sender_username text,
|
||||
-- sender_race_name is the immutable snapshot of the sender's race
|
||||
-- in this game, captured at insert time when sender_kind='player'.
|
||||
-- Admin and system messages carry NULL. The Phase 28 mail UI keys
|
||||
-- per-race threading on this column.
|
||||
sender_race_name text,
|
||||
sender_ip text NOT NULL DEFAULT '',
|
||||
subject text NOT NULL DEFAULT '',
|
||||
body text NOT NULL,
|
||||
@@ -698,6 +703,13 @@ CREATE TABLE diplomail_messages (
|
||||
(sender_kind = 'admin' AND sender_user_id IS NULL AND sender_username IS NOT NULL) OR
|
||||
(sender_kind = 'system' AND sender_user_id IS NULL AND sender_username IS NULL)
|
||||
),
|
||||
-- sender_race_name is only meaningful for player senders. Admin
|
||||
-- and system rows never carry a race; player rows carry one when
|
||||
-- the sender has an active membership at send time (a non-playing
|
||||
-- private-game owner may legitimately have none).
|
||||
CONSTRAINT diplomail_messages_sender_race_chk CHECK (
|
||||
sender_kind = 'player' OR sender_race_name IS NULL
|
||||
),
|
||||
CONSTRAINT diplomail_messages_kind_sender_chk CHECK (
|
||||
(kind = 'personal' AND sender_kind = 'player') OR
|
||||
(kind = 'admin' AND sender_kind IN ('player', 'admin', 'system'))
|
||||
|
||||
@@ -60,19 +60,24 @@ func (h *AdminDiplomailHandlers) Send() gin.HandlerFunc {
|
||||
ctx := c.Request.Context()
|
||||
switch req.Target {
|
||||
case "", "user":
|
||||
recipientID, parseErr := uuid.Parse(req.RecipientUserID)
|
||||
if parseErr != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
var recipientID uuid.UUID
|
||||
if req.RecipientUserID != "" {
|
||||
parsed, parseErr := uuid.Parse(req.RecipientUserID)
|
||||
if parseErr != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
recipientID = parsed
|
||||
}
|
||||
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
|
||||
GameID: gameID,
|
||||
CallerKind: diplomail.CallerKindAdmin,
|
||||
CallerUsername: username,
|
||||
RecipientUserID: recipientID,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
GameID: gameID,
|
||||
CallerKind: diplomail.CallerKindAdmin,
|
||||
CallerUsername: username,
|
||||
RecipientUserID: recipientID,
|
||||
RecipientRaceName: req.RecipientRaceName,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
})
|
||||
if sendErr != nil {
|
||||
respondDiplomailError(c, h.logger, "admin mail send personal", ctx, sendErr)
|
||||
|
||||
@@ -87,19 +87,24 @@ func (h *UserMailHandlers) SendPersonal() gin.HandlerFunc {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "request body must be valid JSON")
|
||||
return
|
||||
}
|
||||
recipientID, err := uuid.Parse(req.RecipientUserID)
|
||||
if err != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
var recipientID uuid.UUID
|
||||
if req.RecipientUserID != "" {
|
||||
parsed, perr := uuid.Parse(req.RecipientUserID)
|
||||
if perr != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
recipientID = parsed
|
||||
}
|
||||
ctx := c.Request.Context()
|
||||
msg, rcpt, err := h.svc.SendPersonal(ctx, diplomail.SendPersonalInput{
|
||||
GameID: gameID,
|
||||
SenderUserID: userID,
|
||||
RecipientUserID: recipientID,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
GameID: gameID,
|
||||
SenderUserID: userID,
|
||||
RecipientUserID: recipientID,
|
||||
RecipientRaceName: req.RecipientRaceName,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
})
|
||||
if err != nil {
|
||||
respondDiplomailError(c, h.logger, "user mail send personal", ctx, err)
|
||||
@@ -189,9 +194,9 @@ func (h *UserMailHandlers) Sent() gin.HandlerFunc {
|
||||
respondDiplomailError(c, h.logger, "user mail sent", ctx, err)
|
||||
return
|
||||
}
|
||||
out := userMailSentListWire{Items: make([]userMailSentSummaryWire, 0, len(items))}
|
||||
for _, m := range items {
|
||||
out.Items = append(out.Items, mailMessageSummaryToWire(m))
|
||||
out := userMailSentListWire{Items: make([]userMailMessageDetailWire, 0, len(items))}
|
||||
for _, entry := range items {
|
||||
out.Items = append(out.Items, mailMessageDetailToWire(entry, false))
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
@@ -341,21 +346,26 @@ func (h *UserMailHandlers) SendAdmin() gin.HandlerFunc {
|
||||
|
||||
switch req.Target {
|
||||
case "", "user":
|
||||
recipientID, parseErr := uuid.Parse(req.RecipientUserID)
|
||||
if parseErr != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
var recipientID uuid.UUID
|
||||
if req.RecipientUserID != "" {
|
||||
parsed, parseErr := uuid.Parse(req.RecipientUserID)
|
||||
if parseErr != nil {
|
||||
httperr.Abort(c, http.StatusBadRequest, httperr.CodeInvalidRequest, "recipient_user_id must be a valid UUID")
|
||||
return
|
||||
}
|
||||
recipientID = parsed
|
||||
}
|
||||
callerUserID := userID
|
||||
msg, rcpt, sendErr := h.svc.SendAdminPersonal(ctx, diplomail.SendAdminPersonalInput{
|
||||
GameID: gameID,
|
||||
CallerKind: diplomail.CallerKindOwner,
|
||||
CallerUserID: &callerUserID,
|
||||
CallerUsername: account.UserName,
|
||||
RecipientUserID: recipientID,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
GameID: gameID,
|
||||
CallerKind: diplomail.CallerKindOwner,
|
||||
CallerUserID: &callerUserID,
|
||||
CallerUsername: account.UserName,
|
||||
RecipientUserID: recipientID,
|
||||
RecipientRaceName: req.RecipientRaceName,
|
||||
Subject: req.Subject,
|
||||
Body: req.Body,
|
||||
SenderIP: clientip.ExtractSourceIP(c),
|
||||
})
|
||||
if sendErr != nil {
|
||||
respondDiplomailError(c, h.logger, "user mail send admin personal", ctx, sendErr)
|
||||
@@ -449,10 +459,13 @@ func parseMessageIDParam(c *gin.Context) (uuid.UUID, bool) {
|
||||
}
|
||||
|
||||
// userMailSendRequestWire mirrors the request body for SendPersonal.
|
||||
// Exactly one of `recipient_user_id` and `recipient_race_name` must
|
||||
// be supplied; the service rejects ambiguous and empty inputs.
|
||||
type userMailSendRequestWire struct {
|
||||
RecipientUserID string `json:"recipient_user_id"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||
RecipientRaceName string `json:"recipient_race_name,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// userMailSendBroadcastRequestWire mirrors the request body for the
|
||||
@@ -464,15 +477,16 @@ type userMailSendBroadcastRequestWire struct {
|
||||
}
|
||||
|
||||
// userMailSendAdminRequestWire mirrors the request body for the
|
||||
// owner-only admin send. `target="user"` requires
|
||||
// `recipient_user_id`; `target="all"` accepts the optional
|
||||
// `recipients` scope (default `active`).
|
||||
// owner-only admin send. `target="user"` requires exactly one of
|
||||
// `recipient_user_id` and `recipient_race_name`; `target="all"`
|
||||
// accepts the optional `recipients` scope (default `active`).
|
||||
type userMailSendAdminRequestWire struct {
|
||||
Target string `json:"target"`
|
||||
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||
Recipients string `json:"recipients,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
Target string `json:"target"`
|
||||
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||
RecipientRaceName string `json:"recipient_race_name,omitempty"`
|
||||
Recipients string `json:"recipients,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// userMailBroadcastReceiptWire is the response shape returned after a
|
||||
@@ -524,6 +538,7 @@ type userMailMessageDetailWire struct {
|
||||
SenderKind string `json:"sender_kind"`
|
||||
SenderUserID *string `json:"sender_user_id,omitempty"`
|
||||
SenderUsername *string `json:"sender_username,omitempty"`
|
||||
SenderRaceName *string `json:"sender_race_name,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
BodyLang string `json:"body_lang"`
|
||||
@@ -540,27 +555,18 @@ type userMailMessageDetailWire struct {
|
||||
Translator *string `json:"translator,omitempty"`
|
||||
}
|
||||
|
||||
// userMailSentSummaryWire mirrors the response shape for the
|
||||
// sender-side listing. Recipient state is intentionally omitted (one
|
||||
// author may have N recipients per broadcast in later stages).
|
||||
type userMailSentSummaryWire struct {
|
||||
MessageID string `json:"message_id"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
BodyLang string `json:"body_lang"`
|
||||
BroadcastScope string `json:"broadcast_scope"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type userMailInboxListWire struct {
|
||||
Items []userMailMessageDetailWire `json:"items"`
|
||||
}
|
||||
|
||||
// userMailSentListWire mirrors the response shape for the
|
||||
// sender-side listing. Phase 28's in-game UI threads sent messages
|
||||
// by the recipient's race name, so the wire carries the full
|
||||
// message detail (including the recipient snapshot) — single sends
|
||||
// contribute one row per message, broadcasts contribute one row per
|
||||
// addressee and the UI collapses them by `message_id`.
|
||||
type userMailSentListWire struct {
|
||||
Items []userMailSentSummaryWire `json:"items"`
|
||||
Items []userMailMessageDetailWire `json:"items"`
|
||||
}
|
||||
|
||||
type userMailUnreadCountWire struct {
|
||||
@@ -597,6 +603,10 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
|
||||
s := *entry.SenderUsername
|
||||
out.SenderUsername = &s
|
||||
}
|
||||
if entry.SenderRaceName != nil {
|
||||
s := *entry.SenderRaceName
|
||||
out.SenderRaceName = &s
|
||||
}
|
||||
if entry.Recipient.RecipientRaceName != nil {
|
||||
s := *entry.Recipient.RecipientRaceName
|
||||
out.RecipientRaceName = &s
|
||||
@@ -624,20 +634,6 @@ func mailMessageDetailToWire(entry diplomail.InboxEntry, justCreated bool) userM
|
||||
return out
|
||||
}
|
||||
|
||||
func mailMessageSummaryToWire(m diplomail.Message) userMailSentSummaryWire {
|
||||
return userMailSentSummaryWire{
|
||||
MessageID: m.MessageID.String(),
|
||||
GameID: m.GameID.String(),
|
||||
GameName: m.GameName,
|
||||
Kind: m.Kind,
|
||||
Subject: m.Subject,
|
||||
Body: m.Body,
|
||||
BodyLang: m.BodyLang,
|
||||
BroadcastScope: m.BroadcastScope,
|
||||
CreatedAt: m.CreatedAt.UTC().Format(timestampLayout),
|
||||
}
|
||||
}
|
||||
|
||||
// mailRecipientStateToWire renders the recipient row after a
|
||||
// mark-read or soft-delete call. The caller only needs the per-user
|
||||
// state, not the full message body again.
|
||||
|
||||
+43
-41
@@ -4068,11 +4068,22 @@ components:
|
||||
UserMailSendRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [recipient_user_id, body]
|
||||
required: [body]
|
||||
properties:
|
||||
recipient_user_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: |
|
||||
Either `recipient_user_id` or `recipient_race_name` must
|
||||
be supplied; supplying both is rejected as
|
||||
`invalid_request`.
|
||||
recipient_race_name:
|
||||
type: string
|
||||
description: |
|
||||
Resolves to the active member with this race name in the
|
||||
game. Mutually exclusive with `recipient_user_id`. The
|
||||
server returns `forbidden` when the matching member is no
|
||||
longer active (lobby-removed / blocked).
|
||||
subject:
|
||||
type: string
|
||||
description: |
|
||||
@@ -4093,10 +4104,18 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
description: |
|
||||
Required when `target="user"`. Identifies the recipient
|
||||
of the personal admin message; the recipient may be in
|
||||
any membership status (admin notifications can reach
|
||||
kicked players).
|
||||
One of `recipient_user_id` and `recipient_race_name` is
|
||||
required when `target="user"`. Identifies the recipient
|
||||
of the personal admin message by uuid; the recipient may
|
||||
be in any membership status (admin notifications can
|
||||
reach kicked players when addressed by user_id).
|
||||
recipient_race_name:
|
||||
type: string
|
||||
description: |
|
||||
Optional alternative to `recipient_user_id` when
|
||||
`target="user"`. Resolves to the active member with this
|
||||
race name in the game; lobby-removed and blocked members
|
||||
cannot be reached through the race-name shortcut.
|
||||
recipients:
|
||||
type: string
|
||||
enum: [active, active_and_removed, all_members]
|
||||
@@ -4323,6 +4342,17 @@ components:
|
||||
sender_username:
|
||||
type: string
|
||||
nullable: true
|
||||
sender_race_name:
|
||||
type: string
|
||||
nullable: true
|
||||
description: |
|
||||
Snapshot of the sender's race name in this game at send
|
||||
time. Populated when `sender_kind="player"` and the
|
||||
sender had an active membership at send time; nil for
|
||||
admin and system messages, and for player messages sent
|
||||
by a private-game owner who was not an active member at
|
||||
send time. The in-game UI keys per-race threading on this
|
||||
field.
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
@@ -4370,41 +4400,6 @@ components:
|
||||
translator:
|
||||
type: string
|
||||
description: Identifier of the translation engine that produced the cached row.
|
||||
UserMailSentSummary:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- message_id
|
||||
- game_id
|
||||
- kind
|
||||
- body
|
||||
- body_lang
|
||||
- broadcast_scope
|
||||
- created_at
|
||||
properties:
|
||||
message_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_name:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum: [personal, admin]
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
body_lang:
|
||||
type: string
|
||||
broadcast_scope:
|
||||
type: string
|
||||
enum: [single, game_broadcast, multi_game_broadcast]
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
UserMailInboxList:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
@@ -4415,6 +4410,13 @@ components:
|
||||
items:
|
||||
$ref: "#/components/schemas/UserMailMessageDetail"
|
||||
UserMailSentList:
|
||||
description: |
|
||||
Sender-side listing of personal messages authored by the
|
||||
caller. Each item carries the same shape as inbox entries
|
||||
(including the recipient snapshot); single sends contribute
|
||||
one row per message, broadcasts contribute one row per
|
||||
addressee so the in-game UI can collapse them by
|
||||
`message_id` into a single stand-alone item.
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [items]
|
||||
@@ -4422,7 +4424,7 @@ components:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/UserMailSentSummary"
|
||||
$ref: "#/components/schemas/UserMailMessageDetail"
|
||||
UserMailUnreadCount:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
||||
@@ -1270,6 +1270,20 @@ The message detail response includes both the original body and,
|
||||
when available, the cached translation; the client UI defaults to
|
||||
the translated text and offers a "show original" toggle.
|
||||
|
||||
The in-game UI groups personal mail into per-race threads — every
|
||||
personal message exchanged between the local player and another
|
||||
race lands in one thread keyed on the other party's race. System
|
||||
mail, admin notifications, and the player's own paid-tier
|
||||
broadcasts render as stand-alone entries in the same list pane and
|
||||
are never threaded. `read_at` and `deleted_at` drive the local
|
||||
unread counter and the soft-delete affordance but are not surfaced
|
||||
to the user — diplomatic mail does not promise read receipts. The
|
||||
compose form picks the recipient by race name (resolved
|
||||
server-side from `Memberships.ListMembers(game_id, "active")`); no
|
||||
client-side memberships listing is fetched. See
|
||||
[`ui/docs/diplomail-ui.md`](../ui/docs/diplomail-ui.md) for the
|
||||
detailed UI breakdown.
|
||||
|
||||
### 11.5 Lifecycle hooks
|
||||
|
||||
Three lobby transitions land as system mail in the affected
|
||||
|
||||
@@ -1309,6 +1309,20 @@ bulk-purge всей почты соответствующей партии.
|
||||
кэш) перевод; UI по умолчанию показывает перевод и предлагает
|
||||
переключение «показать оригинал».
|
||||
|
||||
Внутриигровой UI группирует личную почту по веткам по расам —
|
||||
каждая личная переписка между локальным игроком и другой расой
|
||||
оказывается в одной ветке, ключевая по расе собеседника.
|
||||
Системные сообщения, административные уведомления и собственные
|
||||
рассылки игрока (платный тариф) показываются отдельными
|
||||
автономными записями в том же списке и никогда не группируются.
|
||||
`read_at` и `deleted_at` поддерживают локальный счётчик
|
||||
непрочитанного и кнопку удаления, но не показываются игроку —
|
||||
дипломатическая почта не обещает уведомления о прочтении. Форма
|
||||
compose выбирает получателя по имени расы (сервер резолвит через
|
||||
`Memberships.ListMembers(game_id, "active")`); клиент не тянет
|
||||
отдельный список членов. Подробнее — в
|
||||
[`ui/docs/diplomail-ui.md`](../ui/docs/diplomail-ui.md).
|
||||
|
||||
### 11.5 Хуки жизненного цикла
|
||||
|
||||
Три транзитных перехода в лобби порождают system mail в inbox
|
||||
|
||||
@@ -186,7 +186,8 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
|
||||
userRoutes := backendclient.UserRoutes(backend.REST())
|
||||
lobbyRoutes := backendclient.LobbyRoutes(backend.REST())
|
||||
gameRoutes := backendclient.GameRoutes(backend.REST())
|
||||
allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes))
|
||||
mailRoutes := backendclient.MailRoutes(backend.REST())
|
||||
allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes)+len(gameRoutes)+len(mailRoutes))
|
||||
for k, v := range userRoutes {
|
||||
allRoutes[k] = v
|
||||
}
|
||||
@@ -196,6 +197,9 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
|
||||
for k, v := range gameRoutes {
|
||||
allRoutes[k] = v
|
||||
}
|
||||
for k, v := range mailRoutes {
|
||||
allRoutes[k] = v
|
||||
}
|
||||
|
||||
cleanup := func() error {
|
||||
return closeRedisClient()
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
package backendclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/gateway/internal/downstream"
|
||||
diplomailmodel "galaxy/model/diplomail"
|
||||
commonfbs "galaxy/schema/fbs/common"
|
||||
fbs "galaxy/schema/fbs/diplomail"
|
||||
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ExecuteMailCommand routes one authenticated `user.games.mail.*`
|
||||
// command into the matching `/api/v1/user/games/{game_id}/mail/...`
|
||||
// backend REST endpoint. Each command decodes a FlatBuffers request
|
||||
// payload, issues the REST call, decodes the JSON response, and
|
||||
// re-encodes the result as a typed FlatBuffers envelope.
|
||||
func (c *RESTClient) ExecuteMailCommand(ctx context.Context, command downstream.AuthenticatedCommand) (downstream.UnaryResult, error) {
|
||||
if c == nil || c.httpClient == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil client")
|
||||
}
|
||||
if ctx == nil {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: nil context")
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return downstream.UnaryResult{}, err
|
||||
}
|
||||
if strings.TrimSpace(command.UserID) == "" {
|
||||
return downstream.UnaryResult{}, errors.New("backendclient: execute mail command: user_id must not be empty")
|
||||
}
|
||||
|
||||
switch command.MessageType {
|
||||
case diplomailmodel.MessageTypeUserGamesMailInbox:
|
||||
return c.executeMailInbox(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailSent:
|
||||
return c.executeMailSent(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailMessageGet:
|
||||
return c.executeMailMessageGet(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailSend:
|
||||
return c.executeMailSend(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailBroadcast:
|
||||
return c.executeMailBroadcast(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailAdmin:
|
||||
return c.executeMailAdmin(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailRead:
|
||||
return c.executeMailRead(ctx, command.UserID, command.PayloadBytes)
|
||||
case diplomailmodel.MessageTypeUserGamesMailDelete:
|
||||
return c.executeMailDelete(ctx, command.UserID, command.PayloadBytes)
|
||||
default:
|
||||
return downstream.UnaryResult{}, fmt.Errorf("backendclient: execute mail command: unsupported message type %q", command.MessageType)
|
||||
}
|
||||
}
|
||||
|
||||
// mailMessageJSON mirrors the backend's `UserMailMessageDetail` wire
|
||||
// shape from `backend/openapi.yaml`. Pointer fields are nullable in
|
||||
// the OpenAPI spec; the encoder treats empty strings as "absent".
|
||||
type mailMessageJSON struct {
|
||||
MessageID string `json:"message_id"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
SenderKind string `json:"sender_kind"`
|
||||
SenderUserID *string `json:"sender_user_id,omitempty"`
|
||||
SenderUsername *string `json:"sender_username,omitempty"`
|
||||
SenderRaceName *string `json:"sender_race_name,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
BodyLang string `json:"body_lang"`
|
||||
BroadcastScope string `json:"broadcast_scope"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
RecipientUserID string `json:"recipient_user_id"`
|
||||
RecipientUserName string `json:"recipient_user_name,omitempty"`
|
||||
RecipientRaceName *string `json:"recipient_race_name,omitempty"`
|
||||
ReadAt *string `json:"read_at,omitempty"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
TranslatedSubject *string `json:"translated_subject,omitempty"`
|
||||
TranslatedBody *string `json:"translated_body,omitempty"`
|
||||
TranslationLang *string `json:"translation_lang,omitempty"`
|
||||
Translator *string `json:"translator,omitempty"`
|
||||
}
|
||||
|
||||
// mailRecipientStateJSON mirrors `UserMailRecipientState`.
|
||||
type mailRecipientStateJSON struct {
|
||||
MessageID string `json:"message_id"`
|
||||
ReadAt *string `json:"read_at,omitempty"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
}
|
||||
|
||||
// mailBroadcastReceiptJSON mirrors `UserMailBroadcastReceipt`.
|
||||
type mailBroadcastReceiptJSON struct {
|
||||
MessageID string `json:"message_id"`
|
||||
GameID string `json:"game_id"`
|
||||
GameName string `json:"game_name,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
SenderKind string `json:"sender_kind"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
BodyLang string `json:"body_lang"`
|
||||
BroadcastScope string `json:"broadcast_scope"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
RecipientCount int `json:"recipient_count"`
|
||||
}
|
||||
|
||||
type mailInboxJSON struct {
|
||||
Items []mailMessageJSON `json:"items"`
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailInbox(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsInboxRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
if gameID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.inbox: game_id is missing")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/inbox"
|
||||
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.inbox: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
var resp mailInboxJSON
|
||||
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail inbox response: %w", err)
|
||||
}
|
||||
out := encodeMailMessageList(resp.Items, fbs.InboxResponseStart, fbs.InboxResponseAddItems, fbs.InboxResponseEnd, fbs.FinishInboxResponseBuffer)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailSent(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsSentRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
if gameID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.sent: game_id is missing")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/sent"
|
||||
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.sent: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
var resp mailInboxJSON
|
||||
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail sent response: %w", err)
|
||||
}
|
||||
out := encodeMailMessageList(resp.Items, fbs.SentResponseStart, fbs.SentResponseAddItems, fbs.SentResponseEnd, fbs.FinishSentResponseBuffer)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: out}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailMessageGet(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsMessageGetRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
messageID := readUUID(flat.MessageId(nil))
|
||||
if gameID == uuid.Nil || messageID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.message.get: game_id and message_id are required")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
|
||||
respBody, status, err := c.do(ctx, http.MethodGet, target, userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.message.get: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
var msg mailMessageJSON
|
||||
if err := json.Unmarshal(respBody, &msg); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail message response: %w", err)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(512)
|
||||
msgOff := encodeMailMessage(builder, &msg)
|
||||
fbs.MessageGetResponseStart(builder)
|
||||
fbs.MessageGetResponseAddMessage(builder, msgOff)
|
||||
root := fbs.MessageGetResponseEnd(builder)
|
||||
fbs.FinishMessageGetResponseBuffer(builder, root)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailSend(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsSendRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
if gameID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.send: game_id is missing")
|
||||
}
|
||||
body := struct {
|
||||
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||
RecipientRaceName string `json:"recipient_race_name,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
RecipientUserID: string(flat.RecipientUserId()),
|
||||
RecipientRaceName: string(flat.RecipientRaceName()),
|
||||
Subject: string(flat.Subject()),
|
||||
Body: string(flat.Body()),
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages"
|
||||
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.send: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
var msg mailMessageJSON
|
||||
if err := json.Unmarshal(respBody, &msg); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail send response: %w", err)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(512)
|
||||
msgOff := encodeMailMessage(builder, &msg)
|
||||
fbs.SendResponseStart(builder)
|
||||
fbs.SendResponseAddMessage(builder, msgOff)
|
||||
root := fbs.SendResponseEnd(builder)
|
||||
fbs.FinishSendResponseBuffer(builder, root)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailBroadcast(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsBroadcastRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
if gameID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.broadcast: game_id is missing")
|
||||
}
|
||||
body := struct {
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
Subject: string(flat.Subject()),
|
||||
Body: string(flat.Body()),
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/broadcast"
|
||||
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, body)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.broadcast: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
var receipt mailBroadcastReceiptJSON
|
||||
if err := json.Unmarshal(respBody, &receipt); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail broadcast response: %w", err)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(256)
|
||||
recOff := encodeMailBroadcastReceipt(builder, &receipt)
|
||||
fbs.BroadcastResponseStart(builder)
|
||||
fbs.BroadcastResponseAddReceipt(builder, recOff)
|
||||
root := fbs.BroadcastResponseEnd(builder)
|
||||
fbs.FinishBroadcastResponseBuffer(builder, root)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailAdmin(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsAdminRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
if gameID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.admin: game_id is missing")
|
||||
}
|
||||
target := string(flat.Target())
|
||||
body := struct {
|
||||
Target string `json:"target"`
|
||||
RecipientUserID string `json:"recipient_user_id,omitempty"`
|
||||
RecipientRaceName string `json:"recipient_race_name,omitempty"`
|
||||
Recipients string `json:"recipients,omitempty"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
Target: target,
|
||||
RecipientUserID: string(flat.RecipientUserId()),
|
||||
RecipientRaceName: string(flat.RecipientRaceName()),
|
||||
Recipients: string(flat.Recipients()),
|
||||
Subject: string(flat.Subject()),
|
||||
Body: string(flat.Body()),
|
||||
}
|
||||
url := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/admin"
|
||||
respBody, status, err := c.do(ctx, http.MethodPost, url, userID, body)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.admin: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(512)
|
||||
if target == "all" {
|
||||
var receipt mailBroadcastReceiptJSON
|
||||
if err := json.Unmarshal(respBody, &receipt); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail admin broadcast response: %w", err)
|
||||
}
|
||||
recOff := encodeMailBroadcastReceipt(builder, &receipt)
|
||||
fbs.AdminResponseStart(builder)
|
||||
fbs.AdminResponseAddReceipt(builder, recOff)
|
||||
root := fbs.AdminResponseEnd(builder)
|
||||
fbs.FinishAdminResponseBuffer(builder, root)
|
||||
} else {
|
||||
var msg mailMessageJSON
|
||||
if err := json.Unmarshal(respBody, &msg); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail admin send response: %w", err)
|
||||
}
|
||||
msgOff := encodeMailMessage(builder, &msg)
|
||||
fbs.AdminResponseStart(builder)
|
||||
fbs.AdminResponseAddMessage(builder, msgOff)
|
||||
root := fbs.AdminResponseEnd(builder)
|
||||
fbs.FinishAdminResponseBuffer(builder, root)
|
||||
}
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailRead(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsReadRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
messageID := readUUID(flat.MessageId(nil))
|
||||
if gameID == uuid.Nil || messageID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.read: game_id and message_id are required")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String()) + "/read"
|
||||
respBody, status, err := c.do(ctx, http.MethodPost, target, userID, struct{}{})
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.read: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
return encodeRecipientStateResponse(respBody, fbs.ReadResponseStart, fbs.ReadResponseAddState, fbs.ReadResponseEnd, fbs.FinishReadResponseBuffer)
|
||||
}
|
||||
|
||||
func (c *RESTClient) executeMailDelete(ctx context.Context, userID string, payload []byte) (downstream.UnaryResult, error) {
|
||||
if len(payload) == 0 {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: payload is empty")
|
||||
}
|
||||
flat := fbs.GetRootAsDeleteRequest(payload, 0)
|
||||
gameID := readUUID(flat.GameId(nil))
|
||||
messageID := readUUID(flat.MessageId(nil))
|
||||
if gameID == uuid.Nil || messageID == uuid.Nil {
|
||||
return downstream.UnaryResult{}, errors.New("execute user.games.mail.delete: game_id and message_id are required")
|
||||
}
|
||||
target := c.baseURL + "/api/v1/user/games/" + url.PathEscape(gameID.String()) + "/mail/messages/" + url.PathEscape(messageID.String())
|
||||
respBody, status, err := c.do(ctx, http.MethodDelete, target, userID, nil)
|
||||
if err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("execute user.games.mail.delete: %w", err)
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return projectMailErrorResponse(status, respBody)
|
||||
}
|
||||
return encodeRecipientStateResponse(respBody, fbs.DeleteResponseStart, fbs.DeleteResponseAddState, fbs.DeleteResponseEnd, fbs.FinishDeleteResponseBuffer)
|
||||
}
|
||||
|
||||
// encodeRecipientStateResponse decodes the JSON recipient-state body
|
||||
// and emits the corresponding FlatBuffers Read/Delete envelope. The
|
||||
// caller supplies the trio of envelope start / add-state / end / finish
|
||||
// functions so this helper covers both endpoints with the same shape.
|
||||
func encodeRecipientStateResponse(respBody []byte,
|
||||
startFn func(*flatbuffers.Builder),
|
||||
addStateFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
|
||||
endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
|
||||
finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
|
||||
) (downstream.UnaryResult, error) {
|
||||
var state mailRecipientStateJSON
|
||||
if err := json.Unmarshal(respBody, &state); err != nil {
|
||||
return downstream.UnaryResult{}, fmt.Errorf("decode mail recipient state: %w", err)
|
||||
}
|
||||
builder := flatbuffers.NewBuilder(128)
|
||||
stateOff := encodeMailRecipientState(builder, &state)
|
||||
startFn(builder)
|
||||
addStateFn(builder, stateOff)
|
||||
root := endFn(builder)
|
||||
finishFn(builder, root)
|
||||
return downstream.UnaryResult{ResultCode: userCommandResultCodeOK, PayloadBytes: builder.FinishedBytes()}, nil
|
||||
}
|
||||
|
||||
// encodeMailMessageList is a shared helper that encodes a slice of
|
||||
// mailMessageJSON items into either an InboxResponse or a
|
||||
// SentResponse FlatBuffers envelope. The two envelopes have the same
|
||||
// shape (just a `items` vector of MailMessage) so the trio of
|
||||
// constructor functions parameterises the helper.
|
||||
func encodeMailMessageList(items []mailMessageJSON,
|
||||
startFn func(*flatbuffers.Builder),
|
||||
addItemsFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
|
||||
endFn func(*flatbuffers.Builder) flatbuffers.UOffsetT,
|
||||
finishFn func(*flatbuffers.Builder, flatbuffers.UOffsetT),
|
||||
) []byte {
|
||||
builder := flatbuffers.NewBuilder(1024)
|
||||
offsets := make([]flatbuffers.UOffsetT, 0, len(items))
|
||||
for i := range items {
|
||||
offsets = append(offsets, encodeMailMessage(builder, &items[i]))
|
||||
}
|
||||
// FlatBuffers vectors are built in reverse: prepend each offset.
|
||||
builder.StartVector(4, len(offsets), 4)
|
||||
for i := len(offsets) - 1; i >= 0; i-- {
|
||||
builder.PrependUOffsetT(offsets[i])
|
||||
}
|
||||
itemsVec := builder.EndVector(len(offsets))
|
||||
startFn(builder)
|
||||
addItemsFn(builder, itemsVec)
|
||||
root := endFn(builder)
|
||||
finishFn(builder, root)
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
// encodeMailMessage builds a MailMessage table inside builder. Returns
|
||||
// the offset of the finished table. Strings are interned through the
|
||||
// builder; missing JSON fields (nil pointers, empty strings) yield
|
||||
// empty FB strings which the readers treat as absent.
|
||||
func encodeMailMessage(builder *flatbuffers.Builder, m *mailMessageJSON) flatbuffers.UOffsetT {
|
||||
messageIDOff := builder.CreateString(m.MessageID)
|
||||
gameIDOff := builder.CreateString(m.GameID)
|
||||
gameNameOff := builder.CreateString(m.GameName)
|
||||
kindOff := builder.CreateString(m.Kind)
|
||||
senderKindOff := builder.CreateString(m.SenderKind)
|
||||
senderUserIDOff := builder.CreateString(stringPtrValue(m.SenderUserID))
|
||||
senderUsernameOff := builder.CreateString(stringPtrValue(m.SenderUsername))
|
||||
senderRaceNameOff := builder.CreateString(stringPtrValue(m.SenderRaceName))
|
||||
subjectOff := builder.CreateString(m.Subject)
|
||||
bodyOff := builder.CreateString(m.Body)
|
||||
bodyLangOff := builder.CreateString(m.BodyLang)
|
||||
broadcastScopeOff := builder.CreateString(m.BroadcastScope)
|
||||
recipientUserIDOff := builder.CreateString(m.RecipientUserID)
|
||||
recipientUserNameOff := builder.CreateString(m.RecipientUserName)
|
||||
recipientRaceNameOff := builder.CreateString(stringPtrValue(m.RecipientRaceName))
|
||||
translatedSubjectOff := builder.CreateString(stringPtrValue(m.TranslatedSubject))
|
||||
translatedBodyOff := builder.CreateString(stringPtrValue(m.TranslatedBody))
|
||||
translationLangOff := builder.CreateString(stringPtrValue(m.TranslationLang))
|
||||
translatorOff := builder.CreateString(stringPtrValue(m.Translator))
|
||||
|
||||
fbs.MailMessageStart(builder)
|
||||
fbs.MailMessageAddMessageId(builder, messageIDOff)
|
||||
fbs.MailMessageAddGameId(builder, gameIDOff)
|
||||
fbs.MailMessageAddGameName(builder, gameNameOff)
|
||||
fbs.MailMessageAddKind(builder, kindOff)
|
||||
fbs.MailMessageAddSenderKind(builder, senderKindOff)
|
||||
fbs.MailMessageAddSenderUserId(builder, senderUserIDOff)
|
||||
fbs.MailMessageAddSenderUsername(builder, senderUsernameOff)
|
||||
fbs.MailMessageAddSenderRaceName(builder, senderRaceNameOff)
|
||||
fbs.MailMessageAddSubject(builder, subjectOff)
|
||||
fbs.MailMessageAddBody(builder, bodyOff)
|
||||
fbs.MailMessageAddBodyLang(builder, bodyLangOff)
|
||||
fbs.MailMessageAddBroadcastScope(builder, broadcastScopeOff)
|
||||
fbs.MailMessageAddCreatedAtMs(builder, parseRFC3339Millis(m.CreatedAt))
|
||||
fbs.MailMessageAddRecipientUserId(builder, recipientUserIDOff)
|
||||
fbs.MailMessageAddRecipientUserName(builder, recipientUserNameOff)
|
||||
fbs.MailMessageAddRecipientRaceName(builder, recipientRaceNameOff)
|
||||
fbs.MailMessageAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(m.ReadAt)))
|
||||
fbs.MailMessageAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(m.DeletedAt)))
|
||||
fbs.MailMessageAddTranslatedSubject(builder, translatedSubjectOff)
|
||||
fbs.MailMessageAddTranslatedBody(builder, translatedBodyOff)
|
||||
fbs.MailMessageAddTranslationLang(builder, translationLangOff)
|
||||
fbs.MailMessageAddTranslator(builder, translatorOff)
|
||||
return fbs.MailMessageEnd(builder)
|
||||
}
|
||||
|
||||
// encodeMailRecipientState builds a MailRecipientState table.
|
||||
func encodeMailRecipientState(builder *flatbuffers.Builder, s *mailRecipientStateJSON) flatbuffers.UOffsetT {
|
||||
messageIDOff := builder.CreateString(s.MessageID)
|
||||
fbs.MailRecipientStateStart(builder)
|
||||
fbs.MailRecipientStateAddMessageId(builder, messageIDOff)
|
||||
fbs.MailRecipientStateAddReadAtMs(builder, parseRFC3339Millis(stringPtrValue(s.ReadAt)))
|
||||
fbs.MailRecipientStateAddDeletedAtMs(builder, parseRFC3339Millis(stringPtrValue(s.DeletedAt)))
|
||||
return fbs.MailRecipientStateEnd(builder)
|
||||
}
|
||||
|
||||
// encodeMailBroadcastReceipt builds a MailBroadcastReceipt table.
|
||||
func encodeMailBroadcastReceipt(builder *flatbuffers.Builder, r *mailBroadcastReceiptJSON) flatbuffers.UOffsetT {
|
||||
messageIDOff := builder.CreateString(r.MessageID)
|
||||
gameIDOff := builder.CreateString(r.GameID)
|
||||
gameNameOff := builder.CreateString(r.GameName)
|
||||
kindOff := builder.CreateString(r.Kind)
|
||||
senderKindOff := builder.CreateString(r.SenderKind)
|
||||
subjectOff := builder.CreateString(r.Subject)
|
||||
bodyOff := builder.CreateString(r.Body)
|
||||
bodyLangOff := builder.CreateString(r.BodyLang)
|
||||
broadcastScopeOff := builder.CreateString(r.BroadcastScope)
|
||||
fbs.MailBroadcastReceiptStart(builder)
|
||||
fbs.MailBroadcastReceiptAddMessageId(builder, messageIDOff)
|
||||
fbs.MailBroadcastReceiptAddGameId(builder, gameIDOff)
|
||||
fbs.MailBroadcastReceiptAddGameName(builder, gameNameOff)
|
||||
fbs.MailBroadcastReceiptAddKind(builder, kindOff)
|
||||
fbs.MailBroadcastReceiptAddSenderKind(builder, senderKindOff)
|
||||
fbs.MailBroadcastReceiptAddSubject(builder, subjectOff)
|
||||
fbs.MailBroadcastReceiptAddBody(builder, bodyOff)
|
||||
fbs.MailBroadcastReceiptAddBodyLang(builder, bodyLangOff)
|
||||
fbs.MailBroadcastReceiptAddBroadcastScope(builder, broadcastScopeOff)
|
||||
fbs.MailBroadcastReceiptAddCreatedAtMs(builder, parseRFC3339Millis(r.CreatedAt))
|
||||
fbs.MailBroadcastReceiptAddRecipientCount(builder, int32(r.RecipientCount))
|
||||
return fbs.MailBroadcastReceiptEnd(builder)
|
||||
}
|
||||
|
||||
// projectMailErrorResponse maps a non-2xx response into a UnaryResult
|
||||
// carrying the backend error envelope, reusing the shared user-mail
|
||||
// error-projection. 503 is bubbled as ErrDownstreamUnavailable.
|
||||
func projectMailErrorResponse(statusCode int, payload []byte) (downstream.UnaryResult, error) {
|
||||
if statusCode == http.StatusServiceUnavailable {
|
||||
return downstream.UnaryResult{}, downstream.ErrDownstreamUnavailable
|
||||
}
|
||||
if statusCode >= 400 && statusCode <= 599 {
|
||||
return projectUserBackendError(statusCode, payload)
|
||||
}
|
||||
return downstream.UnaryResult{}, fmt.Errorf("unexpected HTTP status %d", statusCode)
|
||||
}
|
||||
|
||||
// readUUID converts the common.UUID struct (or its absence) into a
|
||||
// google/uuid.UUID. Returns uuid.Nil when the input is nil.
|
||||
func readUUID(u *commonfbs.UUID) uuid.UUID {
|
||||
if u == nil {
|
||||
return uuid.Nil
|
||||
}
|
||||
var out uuid.UUID
|
||||
hi := u.Hi()
|
||||
lo := u.Lo()
|
||||
for i := 0; i < 8; i++ {
|
||||
out[i] = byte(hi >> (56 - 8*i))
|
||||
out[i+8] = byte(lo >> (56 - 8*i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// stringPtrValue returns "" for nil and the dereferenced value
|
||||
// otherwise. Used to flatten nullable JSON strings into the
|
||||
// always-present FlatBuffers string slot.
|
||||
func stringPtrValue(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
// parseRFC3339Millis parses an RFC 3339 timestamp string (the format
|
||||
// the backend mail handler emits) into Unix milliseconds. Returns 0
|
||||
// when the input is empty or unparseable, matching the "absent"
|
||||
// convention for the *_at_ms wire fields.
|
||||
func parseRFC3339Millis(s string) int64 {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return t.UnixMilli()
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package backendclient_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"galaxy/gateway/internal/backendclient"
|
||||
diplomailmodel "galaxy/model/diplomail"
|
||||
commonfbs "galaxy/schema/fbs/common"
|
||||
fbs "galaxy/schema/fbs/diplomail"
|
||||
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExecuteMailInboxDecodesItems(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gameID := uuid.MustParse("11111111-2222-3333-4444-555555555555")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/inbox", r.URL.Path)
|
||||
require.Equal(t, "user-1", r.Header.Get(backendclient.HeaderUserID))
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"items": []map[string]any{
|
||||
{
|
||||
"message_id": "00000000-0000-0000-0000-000000000001",
|
||||
"game_id": gameID.String(),
|
||||
"kind": "personal",
|
||||
"sender_kind": "player",
|
||||
"sender_user_id": "00000000-0000-0000-0000-000000000010",
|
||||
"sender_username": "alice",
|
||||
"sender_race_name": "AliceRace",
|
||||
"subject": "hi",
|
||||
"body": "hello there",
|
||||
"body_lang": "en",
|
||||
"broadcast_scope": "single",
|
||||
"created_at": "2026-05-15T12:00:00Z",
|
||||
"recipient_user_id": "00000000-0000-0000-0000-000000000020",
|
||||
"recipient_user_name": "bob",
|
||||
"recipient_race_name": "BobRace",
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
payload := buildInboxRequest(gameID)
|
||||
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailInbox, payload)
|
||||
result, err := client.ExecuteMailCommand(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", result.ResultCode)
|
||||
|
||||
resp := fbs.GetRootAsInboxResponse(result.PayloadBytes, 0)
|
||||
require.Equal(t, 1, resp.ItemsLength())
|
||||
var item fbs.MailMessage
|
||||
require.True(t, resp.Items(&item, 0))
|
||||
assert.Equal(t, "00000000-0000-0000-0000-000000000001", string(item.MessageId()))
|
||||
assert.Equal(t, "AliceRace", string(item.SenderRaceName()))
|
||||
assert.Equal(t, "BobRace", string(item.RecipientRaceName()))
|
||||
}
|
||||
|
||||
func TestExecuteMailSendForwardsRaceName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gameID := uuid.MustParse("22222222-3333-4444-5555-666666666666")
|
||||
var captured struct {
|
||||
Body string
|
||||
RecipientUserID string
|
||||
RecipientRaceName string
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages", r.URL.Path)
|
||||
var req map[string]any
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, json.Unmarshal(raw, &req))
|
||||
if v, ok := req["body"].(string); ok {
|
||||
captured.Body = v
|
||||
}
|
||||
if v, ok := req["recipient_user_id"].(string); ok {
|
||||
captured.RecipientUserID = v
|
||||
}
|
||||
if v, ok := req["recipient_race_name"].(string); ok {
|
||||
captured.RecipientRaceName = v
|
||||
}
|
||||
writeJSON(t, w, http.StatusCreated, map[string]any{
|
||||
"message_id": "00000000-0000-0000-0000-000000000099",
|
||||
"game_id": gameID.String(),
|
||||
"kind": "personal",
|
||||
"sender_kind": "player",
|
||||
"sender_user_id": "00000000-0000-0000-0000-000000000010",
|
||||
"sender_race_name": "Senders",
|
||||
"body": captured.Body,
|
||||
"body_lang": "en",
|
||||
"broadcast_scope": "single",
|
||||
"created_at": "2026-05-15T12:00:00Z",
|
||||
"recipient_user_id": "00000000-0000-0000-0000-000000000020",
|
||||
"recipient_race_name": "Receivers",
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
payload := buildSendRequestByRaceName(gameID, "Receivers", "let us talk")
|
||||
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailSend, payload)
|
||||
result, err := client.ExecuteMailCommand(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", result.ResultCode)
|
||||
|
||||
resp := fbs.GetRootAsSendResponse(result.PayloadBytes, 0)
|
||||
require.NotNil(t, resp.Message(nil))
|
||||
msg := resp.Message(nil)
|
||||
assert.Equal(t, "let us talk", string(msg.Body()))
|
||||
assert.Equal(t, "Senders", string(msg.SenderRaceName()))
|
||||
assert.Equal(t, "Receivers", string(msg.RecipientRaceName()))
|
||||
|
||||
assert.Empty(t, captured.RecipientUserID)
|
||||
assert.Equal(t, "Receivers", captured.RecipientRaceName)
|
||||
assert.Equal(t, "let us talk", captured.Body)
|
||||
}
|
||||
|
||||
func TestExecuteMailReadReturnsState(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gameID := uuid.MustParse("33333333-4444-5555-6666-777777777777")
|
||||
messageID := uuid.MustParse("00000000-0000-0000-0000-0000000000aa")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
require.Equal(t, "/api/v1/user/games/"+gameID.String()+"/mail/messages/"+messageID.String()+"/read", r.URL.Path)
|
||||
writeJSON(t, w, http.StatusOK, map[string]any{
|
||||
"message_id": messageID.String(),
|
||||
"read_at": "2026-05-15T12:34:56Z",
|
||||
})
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := newRESTClient(t, server)
|
||||
payload := buildReadRequest(gameID, messageID)
|
||||
cmd := newAuthCommand(t, diplomailmodel.MessageTypeUserGamesMailRead, payload)
|
||||
result, err := client.ExecuteMailCommand(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := fbs.GetRootAsReadResponse(result.PayloadBytes, 0)
|
||||
state := resp.State(nil)
|
||||
require.NotNil(t, state)
|
||||
assert.Equal(t, messageID.String(), string(state.MessageId()))
|
||||
assert.NotZero(t, state.ReadAtMs())
|
||||
}
|
||||
|
||||
// buildInboxRequest emits a FlatBuffers InboxRequest envelope with
|
||||
// the supplied game_id.
|
||||
func buildInboxRequest(gameID uuid.UUID) []byte {
|
||||
builder := flatbuffers.NewBuilder(64)
|
||||
hi, lo := uuidToHiLo(gameID)
|
||||
fbs.InboxRequestStart(builder)
|
||||
fbs.InboxRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
root := fbs.InboxRequestEnd(builder)
|
||||
fbs.FinishInboxRequestBuffer(builder, root)
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
// buildSendRequestByRaceName emits a FlatBuffers SendRequest that
|
||||
// addresses the recipient by race name rather than user_id.
|
||||
func buildSendRequestByRaceName(gameID uuid.UUID, raceName, body string) []byte {
|
||||
builder := flatbuffers.NewBuilder(128)
|
||||
raceOff := builder.CreateString(raceName)
|
||||
bodyOff := builder.CreateString(body)
|
||||
hi, lo := uuidToHiLo(gameID)
|
||||
fbs.SendRequestStart(builder)
|
||||
fbs.SendRequestAddGameId(builder, commonfbs.CreateUUID(builder, hi, lo))
|
||||
fbs.SendRequestAddRecipientRaceName(builder, raceOff)
|
||||
fbs.SendRequestAddBody(builder, bodyOff)
|
||||
root := fbs.SendRequestEnd(builder)
|
||||
fbs.FinishSendRequestBuffer(builder, root)
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
// buildReadRequest emits a FlatBuffers ReadRequest envelope.
|
||||
func buildReadRequest(gameID, messageID uuid.UUID) []byte {
|
||||
builder := flatbuffers.NewBuilder(64)
|
||||
gameHi, gameLo := uuidToHiLo(gameID)
|
||||
msgHi, msgLo := uuidToHiLo(messageID)
|
||||
fbs.ReadRequestStart(builder)
|
||||
fbs.ReadRequestAddGameId(builder, commonfbs.CreateUUID(builder, gameHi, gameLo))
|
||||
fbs.ReadRequestAddMessageId(builder, commonfbs.CreateUUID(builder, msgHi, msgLo))
|
||||
root := fbs.ReadRequestEnd(builder)
|
||||
fbs.FinishReadRequestBuffer(builder, root)
|
||||
return builder.FinishedBytes()
|
||||
}
|
||||
|
||||
// uuidToHiLo splits a 16-byte UUID into the two big-endian uint64
|
||||
// halves the common.UUID struct uses.
|
||||
func uuidToHiLo(u uuid.UUID) (uint64, uint64) {
|
||||
var hi, lo uint64
|
||||
for i := 0; i < 8; i++ {
|
||||
hi = (hi << 8) | uint64(u[i])
|
||||
lo = (lo << 8) | uint64(u[i+8])
|
||||
}
|
||||
return hi, lo
|
||||
}
|
||||
@@ -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{}
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type AdminResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsAdminResponse(buf []byte, offset flatbuffers.UOffsetT) *AdminResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &AdminResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishAdminResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsAdminResponse(buf []byte, offset flatbuffers.UOffsetT) *AdminResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &AdminResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedAdminResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *AdminResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *AdminResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *AdminResponse) Message(obj *MailMessage) *MailMessage {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Indirect(o + rcv._tab.Pos)
|
||||
if obj == nil {
|
||||
obj = new(MailMessage)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *AdminResponse) Receipt(obj *MailBroadcastReceipt) *MailBroadcastReceipt {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Indirect(o + rcv._tab.Pos)
|
||||
if obj == nil {
|
||||
obj = new(MailBroadcastReceipt)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AdminResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
}
|
||||
func AdminResponseAddMessage(builder *flatbuffers.Builder, message flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(message), 0)
|
||||
}
|
||||
func AdminResponseAddReceipt(builder *flatbuffers.Builder, receipt flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(receipt), 0)
|
||||
}
|
||||
func AdminResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
common "galaxy/schema/fbs/common"
|
||||
)
|
||||
|
||||
type BroadcastRequest struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsBroadcastRequest(buf []byte, offset flatbuffers.UOffsetT) *BroadcastRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &BroadcastRequest{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishBroadcastRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsBroadcastRequest(buf []byte, offset flatbuffers.UOffsetT) *BroadcastRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &BroadcastRequest{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedBroadcastRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *BroadcastRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *BroadcastRequest) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *BroadcastRequest) GameId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *BroadcastRequest) Subject() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *BroadcastRequest) Body() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BroadcastRequestStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(3)
|
||||
}
|
||||
func BroadcastRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func BroadcastRequestAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(subject), 0)
|
||||
}
|
||||
func BroadcastRequestAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(body), 0)
|
||||
}
|
||||
func BroadcastRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type BroadcastResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsBroadcastResponse(buf []byte, offset flatbuffers.UOffsetT) *BroadcastResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &BroadcastResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishBroadcastResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsBroadcastResponse(buf []byte, offset flatbuffers.UOffsetT) *BroadcastResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &BroadcastResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedBroadcastResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *BroadcastResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *BroadcastResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *BroadcastResponse) Receipt(obj *MailBroadcastReceipt) *MailBroadcastReceipt {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Indirect(o + rcv._tab.Pos)
|
||||
if obj == nil {
|
||||
obj = new(MailBroadcastReceipt)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BroadcastResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func BroadcastResponseAddReceipt(builder *flatbuffers.Builder, receipt flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(receipt), 0)
|
||||
}
|
||||
func BroadcastResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
common "galaxy/schema/fbs/common"
|
||||
)
|
||||
|
||||
type DeleteRequest struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsDeleteRequest(buf []byte, offset flatbuffers.UOffsetT) *DeleteRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &DeleteRequest{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishDeleteRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsDeleteRequest(buf []byte, offset flatbuffers.UOffsetT) *DeleteRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &DeleteRequest{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedDeleteRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *DeleteRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *DeleteRequest) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *DeleteRequest) GameId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *DeleteRequest) MessageId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteRequestStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
}
|
||||
func DeleteRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func DeleteRequestAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(1, flatbuffers.UOffsetT(messageId), 0)
|
||||
}
|
||||
func DeleteRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type DeleteResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsDeleteResponse(buf []byte, offset flatbuffers.UOffsetT) *DeleteResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &DeleteResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishDeleteResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsDeleteResponse(buf []byte, offset flatbuffers.UOffsetT) *DeleteResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &DeleteResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedDeleteResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *DeleteResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *DeleteResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *DeleteResponse) State(obj *MailRecipientState) *MailRecipientState {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Indirect(o + rcv._tab.Pos)
|
||||
if obj == nil {
|
||||
obj = new(MailRecipientState)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func DeleteResponseAddState(builder *flatbuffers.Builder, state flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(state), 0)
|
||||
}
|
||||
func DeleteResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type InboxResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsInboxResponse(buf []byte, offset flatbuffers.UOffsetT) *InboxResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &InboxResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishInboxResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsInboxResponse(buf []byte, offset flatbuffers.UOffsetT) *InboxResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &InboxResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedInboxResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *InboxResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *InboxResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *InboxResponse) Items(obj *MailMessage, j int) bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Vector(o)
|
||||
x += flatbuffers.UOffsetT(j) * 4
|
||||
x = rcv._tab.Indirect(x)
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *InboxResponse) ItemsLength() int {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.VectorLen(o)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func InboxResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func InboxResponseAddItems(builder *flatbuffers.Builder, items flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(items), 0)
|
||||
}
|
||||
func InboxResponseStartItemsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
}
|
||||
func InboxResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type MailBroadcastReceipt struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsMailBroadcastReceipt(buf []byte, offset flatbuffers.UOffsetT) *MailBroadcastReceipt {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &MailBroadcastReceipt{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishMailBroadcastReceiptBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsMailBroadcastReceipt(buf []byte, offset flatbuffers.UOffsetT) *MailBroadcastReceipt {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &MailBroadcastReceipt{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedMailBroadcastReceiptBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) MessageId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) GameId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) GameName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) Kind() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) SenderKind() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) Subject() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) Body() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) BodyLang() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) BroadcastScope() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) CreatedAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) MutateCreatedAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(22, n)
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) RecipientCount() int32 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt32(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailBroadcastReceipt) MutateRecipientCount(n int32) bool {
|
||||
return rcv._tab.MutateInt32Slot(24, n)
|
||||
}
|
||||
|
||||
func MailBroadcastReceiptStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(11)
|
||||
}
|
||||
func MailBroadcastReceiptAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddGameName(builder *flatbuffers.Builder, gameName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(gameName), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddKind(builder *flatbuffers.Builder, kind flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(kind), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddSenderKind(builder *flatbuffers.Builder, senderKind flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(senderKind), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(subject), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(body), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddBodyLang(builder *flatbuffers.Builder, bodyLang flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(bodyLang), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddBroadcastScope(builder *flatbuffers.Builder, broadcastScope flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(8, flatbuffers.UOffsetT(broadcastScope), 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddCreatedAtMs(builder *flatbuffers.Builder, createdAtMs int64) {
|
||||
builder.PrependInt64Slot(9, createdAtMs, 0)
|
||||
}
|
||||
func MailBroadcastReceiptAddRecipientCount(builder *flatbuffers.Builder, recipientCount int32) {
|
||||
builder.PrependInt32Slot(10, recipientCount, 0)
|
||||
}
|
||||
func MailBroadcastReceiptEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type MailMessage struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsMailMessage(buf []byte, offset flatbuffers.UOffsetT) *MailMessage {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &MailMessage{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishMailMessageBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsMailMessage(buf []byte, offset flatbuffers.UOffsetT) *MailMessage {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &MailMessage{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedMailMessageBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) MessageId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) GameId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) GameName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Kind() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) SenderKind() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) SenderUserId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) SenderUsername() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) SenderRaceName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Subject() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Body() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) BodyLang() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) BroadcastScope() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(26))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) CreatedAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(28))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) MutateCreatedAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(28, n)
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) RecipientUserId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) RecipientUserName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) RecipientRaceName() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(34))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) ReadAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(36))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) MutateReadAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(36, n)
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) DeletedAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(38))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) MutateDeletedAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(38, n)
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) TranslatedSubject() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(40))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) TranslatedBody() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(42))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) TranslationLang() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(44))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailMessage) Translator() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(46))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MailMessageStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(22)
|
||||
}
|
||||
func MailMessageAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
|
||||
}
|
||||
func MailMessageAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func MailMessageAddGameName(builder *flatbuffers.Builder, gameName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(gameName), 0)
|
||||
}
|
||||
func MailMessageAddKind(builder *flatbuffers.Builder, kind flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(kind), 0)
|
||||
}
|
||||
func MailMessageAddSenderKind(builder *flatbuffers.Builder, senderKind flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(senderKind), 0)
|
||||
}
|
||||
func MailMessageAddSenderUserId(builder *flatbuffers.Builder, senderUserId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(senderUserId), 0)
|
||||
}
|
||||
func MailMessageAddSenderUsername(builder *flatbuffers.Builder, senderUsername flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(6, flatbuffers.UOffsetT(senderUsername), 0)
|
||||
}
|
||||
func MailMessageAddSenderRaceName(builder *flatbuffers.Builder, senderRaceName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(7, flatbuffers.UOffsetT(senderRaceName), 0)
|
||||
}
|
||||
func MailMessageAddSubject(builder *flatbuffers.Builder, subject flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(8, flatbuffers.UOffsetT(subject), 0)
|
||||
}
|
||||
func MailMessageAddBody(builder *flatbuffers.Builder, body flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(9, flatbuffers.UOffsetT(body), 0)
|
||||
}
|
||||
func MailMessageAddBodyLang(builder *flatbuffers.Builder, bodyLang flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(10, flatbuffers.UOffsetT(bodyLang), 0)
|
||||
}
|
||||
func MailMessageAddBroadcastScope(builder *flatbuffers.Builder, broadcastScope flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(11, flatbuffers.UOffsetT(broadcastScope), 0)
|
||||
}
|
||||
func MailMessageAddCreatedAtMs(builder *flatbuffers.Builder, createdAtMs int64) {
|
||||
builder.PrependInt64Slot(12, createdAtMs, 0)
|
||||
}
|
||||
func MailMessageAddRecipientUserId(builder *flatbuffers.Builder, recipientUserId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(13, flatbuffers.UOffsetT(recipientUserId), 0)
|
||||
}
|
||||
func MailMessageAddRecipientUserName(builder *flatbuffers.Builder, recipientUserName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(14, flatbuffers.UOffsetT(recipientUserName), 0)
|
||||
}
|
||||
func MailMessageAddRecipientRaceName(builder *flatbuffers.Builder, recipientRaceName flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(15, flatbuffers.UOffsetT(recipientRaceName), 0)
|
||||
}
|
||||
func MailMessageAddReadAtMs(builder *flatbuffers.Builder, readAtMs int64) {
|
||||
builder.PrependInt64Slot(16, readAtMs, 0)
|
||||
}
|
||||
func MailMessageAddDeletedAtMs(builder *flatbuffers.Builder, deletedAtMs int64) {
|
||||
builder.PrependInt64Slot(17, deletedAtMs, 0)
|
||||
}
|
||||
func MailMessageAddTranslatedSubject(builder *flatbuffers.Builder, translatedSubject flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(18, flatbuffers.UOffsetT(translatedSubject), 0)
|
||||
}
|
||||
func MailMessageAddTranslatedBody(builder *flatbuffers.Builder, translatedBody flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(19, flatbuffers.UOffsetT(translatedBody), 0)
|
||||
}
|
||||
func MailMessageAddTranslationLang(builder *flatbuffers.Builder, translationLang flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(20, flatbuffers.UOffsetT(translationLang), 0)
|
||||
}
|
||||
func MailMessageAddTranslator(builder *flatbuffers.Builder, translator flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(21, flatbuffers.UOffsetT(translator), 0)
|
||||
}
|
||||
func MailMessageEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type MailRecipientState struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsMailRecipientState(buf []byte, offset flatbuffers.UOffsetT) *MailRecipientState {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &MailRecipientState{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishMailRecipientStateBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsMailRecipientState(buf []byte, offset flatbuffers.UOffsetT) *MailRecipientState {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &MailRecipientState{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedMailRecipientStateBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) MessageId() []byte {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.ByteVector(o + rcv._tab.Pos)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) ReadAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) MutateReadAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(6, n)
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) DeletedAtMs() int64 {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
|
||||
if o != 0 {
|
||||
return rcv._tab.GetInt64(o + rcv._tab.Pos)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (rcv *MailRecipientState) MutateDeletedAtMs(n int64) bool {
|
||||
return rcv._tab.MutateInt64Slot(8, n)
|
||||
}
|
||||
|
||||
func MailRecipientStateStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(3)
|
||||
}
|
||||
func MailRecipientStateAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(messageId), 0)
|
||||
}
|
||||
func MailRecipientStateAddReadAtMs(builder *flatbuffers.Builder, readAtMs int64) {
|
||||
builder.PrependInt64Slot(1, readAtMs, 0)
|
||||
}
|
||||
func MailRecipientStateAddDeletedAtMs(builder *flatbuffers.Builder, deletedAtMs int64) {
|
||||
builder.PrependInt64Slot(2, deletedAtMs, 0)
|
||||
}
|
||||
func MailRecipientStateEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
|
||||
common "galaxy/schema/fbs/common"
|
||||
)
|
||||
|
||||
type MessageGetRequest struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsMessageGetRequest(buf []byte, offset flatbuffers.UOffsetT) *MessageGetRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &MessageGetRequest{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishMessageGetRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsMessageGetRequest(buf []byte, offset flatbuffers.UOffsetT) *MessageGetRequest {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &MessageGetRequest{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedMessageGetRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *MessageGetRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *MessageGetRequest) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *MessageGetRequest) GameId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rcv *MessageGetRequest) MessageId(obj *common.UUID) *common.UUID {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
|
||||
if o != 0 {
|
||||
x := o + rcv._tab.Pos
|
||||
if obj == nil {
|
||||
obj = new(common.UUID)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MessageGetRequestStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(2)
|
||||
}
|
||||
func MessageGetRequestAddGameId(builder *flatbuffers.Builder, gameId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(0, flatbuffers.UOffsetT(gameId), 0)
|
||||
}
|
||||
func MessageGetRequestAddMessageId(builder *flatbuffers.Builder, messageId flatbuffers.UOffsetT) {
|
||||
builder.PrependStructSlot(1, flatbuffers.UOffsetT(messageId), 0)
|
||||
}
|
||||
func MessageGetRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type MessageGetResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsMessageGetResponse(buf []byte, offset flatbuffers.UOffsetT) *MessageGetResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &MessageGetResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishMessageGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsMessageGetResponse(buf []byte, offset flatbuffers.UOffsetT) *MessageGetResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &MessageGetResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedMessageGetResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *MessageGetResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *MessageGetResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *MessageGetResponse) Message(obj *MailMessage) *MailMessage {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Indirect(o + rcv._tab.Pos)
|
||||
if obj == nil {
|
||||
obj = new(MailMessage)
|
||||
}
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return obj
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MessageGetResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func MessageGetResponseAddMessage(builder *flatbuffers.Builder, message flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(message), 0)
|
||||
}
|
||||
func MessageGetResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
|
||||
|
||||
package diplomail
|
||||
|
||||
import (
|
||||
flatbuffers "github.com/google/flatbuffers/go"
|
||||
)
|
||||
|
||||
type SentResponse struct {
|
||||
_tab flatbuffers.Table
|
||||
}
|
||||
|
||||
func GetRootAsSentResponse(buf []byte, offset flatbuffers.UOffsetT) *SentResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset:])
|
||||
x := &SentResponse{}
|
||||
x.Init(buf, n+offset)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSentResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.Finish(offset)
|
||||
}
|
||||
|
||||
func GetSizePrefixedRootAsSentResponse(buf []byte, offset flatbuffers.UOffsetT) *SentResponse {
|
||||
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
|
||||
x := &SentResponse{}
|
||||
x.Init(buf, n+offset+flatbuffers.SizeUint32)
|
||||
return x
|
||||
}
|
||||
|
||||
func FinishSizePrefixedSentResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
|
||||
builder.FinishSizePrefixed(offset)
|
||||
}
|
||||
|
||||
func (rcv *SentResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
|
||||
rcv._tab.Bytes = buf
|
||||
rcv._tab.Pos = i
|
||||
}
|
||||
|
||||
func (rcv *SentResponse) Table() flatbuffers.Table {
|
||||
return rcv._tab
|
||||
}
|
||||
|
||||
func (rcv *SentResponse) Items(obj *MailMessage, j int) bool {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
x := rcv._tab.Vector(o)
|
||||
x += flatbuffers.UOffsetT(j) * 4
|
||||
x = rcv._tab.Indirect(x)
|
||||
obj.Init(rcv._tab.Bytes, x)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rcv *SentResponse) ItemsLength() int {
|
||||
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
|
||||
if o != 0 {
|
||||
return rcv._tab.VectorLen(o)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func SentResponseStart(builder *flatbuffers.Builder) {
|
||||
builder.StartObject(1)
|
||||
}
|
||||
func SentResponseAddItems(builder *flatbuffers.Builder, items flatbuffers.UOffsetT) {
|
||||
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(items), 0)
|
||||
}
|
||||
func SentResponseStartItemsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
|
||||
return builder.StartVector(4, numElems, 4)
|
||||
}
|
||||
func SentResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
|
||||
return builder.EndObject()
|
||||
}
|
||||
+1
-1
@@ -6,7 +6,7 @@ WASM_OUT := frontend/static/core.wasm
|
||||
WASM_EXEC := frontend/static/wasm_exec.js
|
||||
TINYGO_ROOT := $(shell tinygo env TINYGOROOT 2>/dev/null)
|
||||
FBS_OUT := frontend/src/proto/galaxy/fbs
|
||||
FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs
|
||||
FBS_INPUTS := ../pkg/schema/fbs/common.fbs ../pkg/schema/fbs/lobby.fbs ../pkg/schema/fbs/user.fbs ../pkg/schema/fbs/report.fbs ../pkg/schema/fbs/order.fbs ../pkg/schema/fbs/diplomail.fbs
|
||||
|
||||
help:
|
||||
@echo "ui targets:"
|
||||
|
||||
+66
-2
@@ -3070,9 +3070,73 @@ bottom):
|
||||
- animated transitions when survivors re-distribute after an
|
||||
elimination (currently hard-jumps).
|
||||
|
||||
## Phase 28. Diplomatic Mail View
|
||||
## ~~Phase 28. Diplomatic Mail View~~
|
||||
|
||||
Status: pending.
|
||||
Status: done (CI gate passed on run 136 — go-unit / ui-test / integration all green at commit 6d0272b).
|
||||
|
||||
Decisions baked in during implementation:
|
||||
|
||||
1. **Transport: ConnectRPC `user.games.mail.*`.** Eight new
|
||||
authenticated commands (inbox / sent / message.get / send /
|
||||
broadcast / admin / read / delete) plumbed end-to-end through
|
||||
the existing gateway → backend REST surface. Schemas in
|
||||
`pkg/schema/fbs/diplomail.fbs`; constants in
|
||||
`pkg/model/diplomail/diplomail.go`; gateway translation in
|
||||
`gateway/internal/backendclient/mail_commands.go`.
|
||||
2. **Recipient by race name.** The send / admin endpoints accept
|
||||
an alternative `recipient_race_name` field; backend resolves it
|
||||
via `Memberships.ListMembers(gameID, "active")`. The UI feeds
|
||||
the picker straight off `report.races[].name` — no client-side
|
||||
memberships RPC.
|
||||
3. **`sender_race_name` snapshot.** New nullable column on
|
||||
`diplomail_messages`, populated for `sender_kind='player'`
|
||||
senders that have an active membership at send time. Drives the
|
||||
per-race threading on the client.
|
||||
4. **/sent returns full message detail.** Backend's bulk sent
|
||||
listing now returns the same `UserMailMessageDetail` shape as
|
||||
`/inbox`, one row per (message, recipient). The UI collapses
|
||||
broadcasts by `message_id` into a single stand-alone item.
|
||||
5. **Threading + stand-alones.** `MailStore.entries` groups
|
||||
personal messages by the other party's race name. System,
|
||||
admin, and outgoing broadcasts render as stand-alone items in
|
||||
the same list pane.
|
||||
6. **No read receipts.** `read_at` and `deleted_at` drive the
|
||||
badge counter and soft-delete affordance but are never shown
|
||||
to the user.
|
||||
7. **Header badge.** Inline pill on the view-menu "diplomatic
|
||||
mail" row, fed by `mailStore.unreadCount`. No always-visible
|
||||
chrome added.
|
||||
8. **Push event reuse.** A new
|
||||
`eventStream.on("diplomail.message.received", …)` handler in
|
||||
`routes/games/[id]/+layout.svelte` parses the verified payload,
|
||||
refreshes the inbox, and raises a `toast.show` with a "view"
|
||||
deep-link.
|
||||
9. **Translation toggle.** Per-message Show original / Show
|
||||
translation toggle inside both `thread-pane.svelte` and
|
||||
`system-item-pane.svelte`; the body defaults to the cached
|
||||
translation when present.
|
||||
|
||||
Artifacts (delivered):
|
||||
|
||||
- backend: `internal/postgres/migrations/00001_init.sql`,
|
||||
`internal/diplomail/{types.go,store.go,service.go,admin_send.go,diplomail_e2e_test.go,README.md}`,
|
||||
`internal/server/{handlers_user_mail.go,handlers_admin_diplomail.go}`,
|
||||
`openapi.yaml`;
|
||||
- wire: `pkg/schema/fbs/diplomail.fbs` + generated Go and TS
|
||||
bindings; `pkg/model/diplomail/diplomail.go`;
|
||||
- gateway: `gateway/internal/backendclient/{mail_commands.go,routes.go,mail_commands_test.go}`,
|
||||
`gateway/cmd/gateway/main.go`;
|
||||
- ui: `ui/frontend/src/api/diplomail.ts`,
|
||||
`ui/frontend/src/lib/mail-store.svelte.ts`,
|
||||
`ui/frontend/src/lib/active-view/mail.svelte` (+ subdir
|
||||
`mail/{thread-list,thread-pane,system-item-pane,compose,system-titles}.svelte|.ts`),
|
||||
`ui/frontend/src/lib/header/view-menu.svelte`,
|
||||
`ui/frontend/src/routes/games/[id]/+layout.svelte`,
|
||||
`ui/frontend/src/lib/i18n/locales/{en,ru}.ts`;
|
||||
- docs: `ui/docs/diplomail-ui.md`, `docs/FUNCTIONAL.md` §11.4 +
|
||||
mirror in `docs/FUNCTIONAL_ru.md`.
|
||||
|
||||
Original phase brief follows.
|
||||
|
||||
Goal: implement a mail inbox and compose flow as a dedicated view that
|
||||
replaces the map.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# In-game diplomatic mail UI
|
||||
|
||||
Phase 28 wires the in-game mail view that consumes the `diplomail`
|
||||
subsystem in the backend. The route lives at `/games/:id/mail`
|
||||
(registered in Phase 10) and replaces the active view when the user
|
||||
opens the "diplomatic mail" entry in the header menu.
|
||||
|
||||
## Wire surface
|
||||
|
||||
Eight ConnectRPC commands sit between UI and backend, all under the
|
||||
`user.games.mail.*` namespace:
|
||||
|
||||
| Command | Backend REST endpoint |
|
||||
|---|---|
|
||||
| `user.games.mail.inbox` | `GET /api/v1/user/games/{id}/mail/inbox` |
|
||||
| `user.games.mail.sent` | `GET …/mail/sent` |
|
||||
| `user.games.mail.message.get` | `GET …/mail/messages/{message_id}` |
|
||||
| `user.games.mail.send` | `POST …/mail/messages` |
|
||||
| `user.games.mail.broadcast` | `POST …/mail/broadcast` |
|
||||
| `user.games.mail.admin` | `POST …/mail/admin` |
|
||||
| `user.games.mail.read` | `POST …/mail/messages/{id}/read` |
|
||||
| `user.games.mail.delete` | `DELETE …/mail/messages/{id}` |
|
||||
|
||||
The FlatBuffers schemas live under
|
||||
[`pkg/schema/fbs/diplomail.fbs`](../../pkg/schema/fbs/diplomail.fbs);
|
||||
the gateway translation lives in
|
||||
[`gateway/internal/backendclient/mail_commands.go`](../../gateway/internal/backendclient/mail_commands.go).
|
||||
|
||||
## Recipient by race name
|
||||
|
||||
The compose flow does **not** consult a memberships listing. The
|
||||
recipient picker reads `gameState.report.races[].name` (the Phase 22
|
||||
projection of `report.player[]`), and the send request carries the
|
||||
chosen race name as `recipient_race_name`. The backend resolves it
|
||||
against `Memberships.ListMembers(gameID, "active")` and rejects with
|
||||
`forbidden` if the matching member is no longer active. This keeps
|
||||
the UI off the lobby surface for the common case.
|
||||
|
||||
## Threading model
|
||||
|
||||
`MailStore.entries` is the derived rune the active view consumes. It
|
||||
projects the union of inbox and sent into:
|
||||
|
||||
- **Per-race threads** — every personal message keyed by another
|
||||
race contributes to a thread keyed on that race name. Incoming is
|
||||
keyed on `sender_race_name`; outgoing is keyed on
|
||||
`recipient_race_name`. Thread messages are sorted oldest → newest
|
||||
for chat-style rendering; the unread badge counts incoming
|
||||
`read_at === null` rows only.
|
||||
- **Stand-alone items** — system mail (`sender_kind=system`), admin
|
||||
notifications (`sender_kind=admin`), and the caller's own
|
||||
paid-tier broadcasts (`broadcast_scope=game_broadcast`). Backend
|
||||
returns one row per recipient for paid-tier broadcasts; the UI
|
||||
collapses them by `message_id` into a single stand-alone item.
|
||||
|
||||
`read_at` and `deleted_at` are not surfaced to the user in any pane
|
||||
— they only drive the badge counter and the optimistic mark-read
|
||||
state. This is intentional (per Phase 28 decisions): the user-facing
|
||||
spec for diplomatic mail does not promise read receipts.
|
||||
|
||||
## Translation toggle
|
||||
|
||||
When a message detail carries `translated_body`, the body and (if
|
||||
non-empty) subject default to the translated rendering. Each message
|
||||
pane exposes a "Show original" / "Show translation" button that
|
||||
flips the per-message state. Messages without a cached translation
|
||||
render the original directly with no toggle.
|
||||
|
||||
## Push events
|
||||
|
||||
`diplomail.message.received` push frames are dispatched from
|
||||
`api/events.svelte.ts` via the singleton SubscribeEvents stream. The
|
||||
in-game layout (`routes/games/[id]/+layout.svelte`) parses the
|
||||
verified payload, calls `mailStore.applyPushEvent(gameId)` (which
|
||||
re-fetches the inbox — the payload only carries a preview), and
|
||||
raises a toast through `lib/toast.svelte.ts` with a "view"
|
||||
deep-link to `/games/:id/mail`.
|
||||
|
||||
The header view-menu's mail entry shows `mailStore.unreadCount` as
|
||||
an inline pill — the only chrome the badge needs.
|
||||
|
||||
## Layout
|
||||
|
||||
Desktop (≥ 768 px) renders a two-pane CSS grid: list on the left,
|
||||
detail on the right. Mobile flips to a single-pane stack; tapping a
|
||||
list row hides the list and shows the detail with a back button.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Bodies render through Svelte's default text-content path (no HTML
|
||||
parsing) per the backend rule of treating message text as plain
|
||||
UTF-8.
|
||||
- The compose dialog uses native form controls; the recipient
|
||||
picker is a `<select>` so screen-readers and keyboard users get
|
||||
the standard semantics.
|
||||
- The reply box and the compose body are real `<textarea>`s so
|
||||
shift-enter newlines, paste, and selection behave correctly.
|
||||
@@ -0,0 +1,421 @@
|
||||
// Typed wrappers around `GalaxyClient.executeCommand` for the eight
|
||||
// `user.games.mail.*` Phase 28 ConnectRPC commands. Each wrapper
|
||||
// builds the matching FlatBuffers request, decodes the FlatBuffers
|
||||
// response, and surfaces backend errors through `MailError` so callers
|
||||
// branch on canonical codes (`invalid_request`, `forbidden`,
|
||||
// `not_found`, `conflict`).
|
||||
|
||||
import { Builder, ByteBuffer } from "flatbuffers";
|
||||
|
||||
import type { GalaxyClient } from "./galaxy-client";
|
||||
import { uuidToHiLo } from "./game-state";
|
||||
import {
|
||||
AdminRequest,
|
||||
AdminResponse,
|
||||
BroadcastRequest,
|
||||
BroadcastResponse,
|
||||
DeleteRequest,
|
||||
DeleteResponse,
|
||||
InboxRequest,
|
||||
InboxResponse,
|
||||
MailMessage as FbsMailMessage,
|
||||
MailRecipientState as FbsMailRecipientState,
|
||||
MailBroadcastReceipt as FbsMailBroadcastReceipt,
|
||||
MessageGetRequest,
|
||||
MessageGetResponse,
|
||||
ReadRequest,
|
||||
ReadResponse,
|
||||
SendRequest,
|
||||
SendResponse,
|
||||
SentRequest,
|
||||
SentResponse,
|
||||
} from "../proto/galaxy/fbs/diplomail";
|
||||
import { UUID } from "../proto/galaxy/fbs/common";
|
||||
import { ErrorResponse as FbsErrorResponse } from "../proto/galaxy/fbs/lobby";
|
||||
|
||||
/**
|
||||
* MailError represents a non-`ok` response from a mail RPC. Callers
|
||||
* branch on `code` for canonical error handling and use `message` for
|
||||
* inline UI surfacing.
|
||||
*/
|
||||
export class MailError extends Error {
|
||||
readonly resultCode: string;
|
||||
readonly code: string;
|
||||
|
||||
constructor(resultCode: string, code: string, message: string) {
|
||||
super(message);
|
||||
this.name = "MailError";
|
||||
this.resultCode = resultCode;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MailMessage is the typed UI view of a `MailMessage` FlatBuffers row.
|
||||
* Nullable wire fields (`sender_user_id`, timestamps, translation
|
||||
* slots) become `null` here; the empty string from FB readers is
|
||||
* normalised to either `""` or `null` based on field semantics.
|
||||
*/
|
||||
export interface MailMessage {
|
||||
messageId: string;
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
kind: string;
|
||||
senderKind: string;
|
||||
senderUserId: string | null;
|
||||
senderUsername: string | null;
|
||||
senderRaceName: string | null;
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyLang: string;
|
||||
broadcastScope: string;
|
||||
createdAt: Date;
|
||||
recipientUserId: string;
|
||||
recipientUserName: string;
|
||||
recipientRaceName: string | null;
|
||||
readAt: Date | null;
|
||||
deletedAt: Date | null;
|
||||
translatedSubject: string | null;
|
||||
translatedBody: string | null;
|
||||
translationLang: string | null;
|
||||
translator: string | null;
|
||||
}
|
||||
|
||||
export interface MailRecipientState {
|
||||
messageId: string;
|
||||
readAt: Date | null;
|
||||
deletedAt: Date | null;
|
||||
}
|
||||
|
||||
export interface MailBroadcastReceipt {
|
||||
messageId: string;
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
kind: string;
|
||||
senderKind: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyLang: string;
|
||||
broadcastScope: string;
|
||||
createdAt: Date;
|
||||
recipientCount: number;
|
||||
}
|
||||
|
||||
export interface SendPersonalArgs {
|
||||
gameId: string;
|
||||
raceName: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface SendBroadcastArgs {
|
||||
gameId: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type AdminTarget = "user" | "all";
|
||||
|
||||
export interface SendAdminArgs {
|
||||
gameId: string;
|
||||
target: AdminTarget;
|
||||
raceName?: string;
|
||||
recipientUserId?: string;
|
||||
recipients?: string;
|
||||
subject?: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const MESSAGE_TYPE_INBOX = "user.games.mail.inbox";
|
||||
const MESSAGE_TYPE_SENT = "user.games.mail.sent";
|
||||
const MESSAGE_TYPE_GET = "user.games.mail.message.get";
|
||||
const MESSAGE_TYPE_SEND = "user.games.mail.send";
|
||||
const MESSAGE_TYPE_BROADCAST = "user.games.mail.broadcast";
|
||||
const MESSAGE_TYPE_ADMIN = "user.games.mail.admin";
|
||||
const MESSAGE_TYPE_READ = "user.games.mail.read";
|
||||
const MESSAGE_TYPE_DELETE = "user.games.mail.delete";
|
||||
|
||||
const RESULT_CODE_OK = "ok";
|
||||
|
||||
export async function fetchInbox(
|
||||
client: GalaxyClient,
|
||||
gameId: string,
|
||||
): Promise<MailMessage[]> {
|
||||
const builder = new Builder(64);
|
||||
const [hi, lo] = uuidToHiLo(gameId);
|
||||
InboxRequest.startInboxRequest(builder);
|
||||
InboxRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
|
||||
builder.finish(InboxRequest.endInboxRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_INBOX, builder.asUint8Array());
|
||||
const response = InboxResponse.getRootAsInboxResponse(new ByteBuffer(payload));
|
||||
return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
|
||||
}
|
||||
|
||||
export async function fetchSent(
|
||||
client: GalaxyClient,
|
||||
gameId: string,
|
||||
): Promise<MailMessage[]> {
|
||||
const builder = new Builder(64);
|
||||
const [hi, lo] = uuidToHiLo(gameId);
|
||||
SentRequest.startSentRequest(builder);
|
||||
SentRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
|
||||
builder.finish(SentRequest.endSentRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_SENT, builder.asUint8Array());
|
||||
const response = SentResponse.getRootAsSentResponse(new ByteBuffer(payload));
|
||||
return readMessageList(response.itemsLength.bind(response), (i) => response.items(i));
|
||||
}
|
||||
|
||||
export async function fetchMessage(
|
||||
client: GalaxyClient,
|
||||
gameId: string,
|
||||
messageId: string,
|
||||
): Promise<MailMessage> {
|
||||
const builder = new Builder(64);
|
||||
const [ghi, glo] = uuidToHiLo(gameId);
|
||||
const [mhi, mlo] = uuidToHiLo(messageId);
|
||||
MessageGetRequest.startMessageGetRequest(builder);
|
||||
MessageGetRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
|
||||
MessageGetRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
|
||||
builder.finish(MessageGetRequest.endMessageGetRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_GET, builder.asUint8Array());
|
||||
const response = MessageGetResponse.getRootAsMessageGetResponse(new ByteBuffer(payload));
|
||||
const fb = response.message();
|
||||
if (fb === null) {
|
||||
throw new MailError("internal_error", "internal_error", "message missing in response");
|
||||
}
|
||||
return decodeMailMessage(fb);
|
||||
}
|
||||
|
||||
export async function sendPersonal(
|
||||
client: GalaxyClient,
|
||||
input: SendPersonalArgs,
|
||||
): Promise<MailMessage> {
|
||||
const builder = new Builder(256);
|
||||
const [hi, lo] = uuidToHiLo(input.gameId);
|
||||
const raceOff = builder.createString(input.raceName);
|
||||
const subjectOff = builder.createString(input.subject ?? "");
|
||||
const bodyOff = builder.createString(input.body);
|
||||
SendRequest.startSendRequest(builder);
|
||||
SendRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
|
||||
SendRequest.addRecipientRaceName(builder, raceOff);
|
||||
SendRequest.addSubject(builder, subjectOff);
|
||||
SendRequest.addBody(builder, bodyOff);
|
||||
builder.finish(SendRequest.endSendRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_SEND, builder.asUint8Array());
|
||||
const response = SendResponse.getRootAsSendResponse(new ByteBuffer(payload));
|
||||
const fb = response.message();
|
||||
if (fb === null) {
|
||||
throw new MailError("internal_error", "internal_error", "message missing in response");
|
||||
}
|
||||
return decodeMailMessage(fb);
|
||||
}
|
||||
|
||||
export async function sendBroadcast(
|
||||
client: GalaxyClient,
|
||||
input: SendBroadcastArgs,
|
||||
): Promise<MailBroadcastReceipt> {
|
||||
const builder = new Builder(256);
|
||||
const [hi, lo] = uuidToHiLo(input.gameId);
|
||||
const subjectOff = builder.createString(input.subject ?? "");
|
||||
const bodyOff = builder.createString(input.body);
|
||||
BroadcastRequest.startBroadcastRequest(builder);
|
||||
BroadcastRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
|
||||
BroadcastRequest.addSubject(builder, subjectOff);
|
||||
BroadcastRequest.addBody(builder, bodyOff);
|
||||
builder.finish(BroadcastRequest.endBroadcastRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_BROADCAST, builder.asUint8Array());
|
||||
const response = BroadcastResponse.getRootAsBroadcastResponse(new ByteBuffer(payload));
|
||||
const fb = response.receipt();
|
||||
if (fb === null) {
|
||||
throw new MailError("internal_error", "internal_error", "receipt missing in response");
|
||||
}
|
||||
return decodeMailBroadcastReceipt(fb);
|
||||
}
|
||||
|
||||
export async function sendAdmin(
|
||||
client: GalaxyClient,
|
||||
input: SendAdminArgs,
|
||||
): Promise<MailMessage | MailBroadcastReceipt> {
|
||||
const builder = new Builder(256);
|
||||
const [hi, lo] = uuidToHiLo(input.gameId);
|
||||
const targetOff = builder.createString(input.target);
|
||||
const recipientUserOff = builder.createString(input.recipientUserId ?? "");
|
||||
const recipientRaceOff = builder.createString(input.raceName ?? "");
|
||||
const recipientsOff = builder.createString(input.recipients ?? "");
|
||||
const subjectOff = builder.createString(input.subject ?? "");
|
||||
const bodyOff = builder.createString(input.body);
|
||||
AdminRequest.startAdminRequest(builder);
|
||||
AdminRequest.addGameId(builder, UUID.createUUID(builder, hi, lo));
|
||||
AdminRequest.addTarget(builder, targetOff);
|
||||
AdminRequest.addRecipientUserId(builder, recipientUserOff);
|
||||
AdminRequest.addRecipientRaceName(builder, recipientRaceOff);
|
||||
AdminRequest.addRecipients(builder, recipientsOff);
|
||||
AdminRequest.addSubject(builder, subjectOff);
|
||||
AdminRequest.addBody(builder, bodyOff);
|
||||
builder.finish(AdminRequest.endAdminRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_ADMIN, builder.asUint8Array());
|
||||
const response = AdminResponse.getRootAsAdminResponse(new ByteBuffer(payload));
|
||||
const receipt = response.receipt();
|
||||
if (receipt !== null) {
|
||||
return decodeMailBroadcastReceipt(receipt);
|
||||
}
|
||||
const message = response.message();
|
||||
if (message !== null) {
|
||||
return decodeMailMessage(message);
|
||||
}
|
||||
throw new MailError("internal_error", "internal_error", "admin response carried neither message nor receipt");
|
||||
}
|
||||
|
||||
export async function markRead(
|
||||
client: GalaxyClient,
|
||||
gameId: string,
|
||||
messageId: string,
|
||||
): Promise<MailRecipientState> {
|
||||
const builder = new Builder(64);
|
||||
const [ghi, glo] = uuidToHiLo(gameId);
|
||||
const [mhi, mlo] = uuidToHiLo(messageId);
|
||||
ReadRequest.startReadRequest(builder);
|
||||
ReadRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
|
||||
ReadRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
|
||||
builder.finish(ReadRequest.endReadRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_READ, builder.asUint8Array());
|
||||
const response = ReadResponse.getRootAsReadResponse(new ByteBuffer(payload));
|
||||
const fb = response.state();
|
||||
if (fb === null) {
|
||||
throw new MailError("internal_error", "internal_error", "state missing in response");
|
||||
}
|
||||
return decodeMailRecipientState(fb);
|
||||
}
|
||||
|
||||
export async function deleteMessage(
|
||||
client: GalaxyClient,
|
||||
gameId: string,
|
||||
messageId: string,
|
||||
): Promise<MailRecipientState> {
|
||||
const builder = new Builder(64);
|
||||
const [ghi, glo] = uuidToHiLo(gameId);
|
||||
const [mhi, mlo] = uuidToHiLo(messageId);
|
||||
DeleteRequest.startDeleteRequest(builder);
|
||||
DeleteRequest.addGameId(builder, UUID.createUUID(builder, ghi, glo));
|
||||
DeleteRequest.addMessageId(builder, UUID.createUUID(builder, mhi, mlo));
|
||||
builder.finish(DeleteRequest.endDeleteRequest(builder));
|
||||
const payload = await execute(client, MESSAGE_TYPE_DELETE, builder.asUint8Array());
|
||||
const response = DeleteResponse.getRootAsDeleteResponse(new ByteBuffer(payload));
|
||||
const fb = response.state();
|
||||
if (fb === null) {
|
||||
throw new MailError("internal_error", "internal_error", "state missing in response");
|
||||
}
|
||||
return decodeMailRecipientState(fb);
|
||||
}
|
||||
|
||||
async function execute(
|
||||
client: GalaxyClient,
|
||||
messageType: string,
|
||||
payloadBytes: Uint8Array,
|
||||
): Promise<Uint8Array> {
|
||||
const result = await client.executeCommand(messageType, payloadBytes);
|
||||
if (result.resultCode !== RESULT_CODE_OK) {
|
||||
throw decodeMailError(result.resultCode, result.payloadBytes);
|
||||
}
|
||||
return result.payloadBytes;
|
||||
}
|
||||
|
||||
function decodeMailError(resultCode: string, payload: Uint8Array): MailError {
|
||||
let code = resultCode;
|
||||
let message = resultCode;
|
||||
try {
|
||||
const errorResponse = FbsErrorResponse.getRootAsErrorResponse(new ByteBuffer(payload));
|
||||
const body = errorResponse.error();
|
||||
if (body) {
|
||||
code = body.code() ?? resultCode;
|
||||
message = body.message() ?? resultCode;
|
||||
}
|
||||
} catch (_err) {
|
||||
// fall through to use raw resultCode
|
||||
}
|
||||
return new MailError(resultCode, code, message);
|
||||
}
|
||||
|
||||
function readMessageList(
|
||||
lengthFn: () => number,
|
||||
getFn: (i: number) => FbsMailMessage | null,
|
||||
): MailMessage[] {
|
||||
const total = lengthFn();
|
||||
const out: MailMessage[] = [];
|
||||
for (let i = 0; i < total; i++) {
|
||||
const item = getFn(i);
|
||||
if (item) {
|
||||
out.push(decodeMailMessage(item));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeMailMessage(fb: FbsMailMessage): MailMessage {
|
||||
return {
|
||||
messageId: fb.messageId() ?? "",
|
||||
gameId: fb.gameId() ?? "",
|
||||
gameName: fb.gameName() ?? "",
|
||||
kind: fb.kind() ?? "",
|
||||
senderKind: fb.senderKind() ?? "",
|
||||
senderUserId: optionalString(fb.senderUserId()),
|
||||
senderUsername: optionalString(fb.senderUsername()),
|
||||
senderRaceName: optionalString(fb.senderRaceName()),
|
||||
subject: fb.subject() ?? "",
|
||||
body: fb.body() ?? "",
|
||||
bodyLang: fb.bodyLang() ?? "",
|
||||
broadcastScope: fb.broadcastScope() ?? "",
|
||||
createdAt: dateFromMs(fb.createdAtMs()),
|
||||
recipientUserId: fb.recipientUserId() ?? "",
|
||||
recipientUserName: fb.recipientUserName() ?? "",
|
||||
recipientRaceName: optionalString(fb.recipientRaceName()),
|
||||
readAt: optionalDateFromMs(fb.readAtMs()),
|
||||
deletedAt: optionalDateFromMs(fb.deletedAtMs()),
|
||||
translatedSubject: optionalString(fb.translatedSubject()),
|
||||
translatedBody: optionalString(fb.translatedBody()),
|
||||
translationLang: optionalString(fb.translationLang()),
|
||||
translator: optionalString(fb.translator()),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeMailRecipientState(fb: FbsMailRecipientState): MailRecipientState {
|
||||
return {
|
||||
messageId: fb.messageId() ?? "",
|
||||
readAt: optionalDateFromMs(fb.readAtMs()),
|
||||
deletedAt: optionalDateFromMs(fb.deletedAtMs()),
|
||||
};
|
||||
}
|
||||
|
||||
function decodeMailBroadcastReceipt(fb: FbsMailBroadcastReceipt): MailBroadcastReceipt {
|
||||
return {
|
||||
messageId: fb.messageId() ?? "",
|
||||
gameId: fb.gameId() ?? "",
|
||||
gameName: fb.gameName() ?? "",
|
||||
kind: fb.kind() ?? "",
|
||||
senderKind: fb.senderKind() ?? "",
|
||||
subject: fb.subject() ?? "",
|
||||
body: fb.body() ?? "",
|
||||
bodyLang: fb.bodyLang() ?? "",
|
||||
broadcastScope: fb.broadcastScope() ?? "",
|
||||
createdAt: dateFromMs(fb.createdAtMs()),
|
||||
recipientCount: fb.recipientCount(),
|
||||
};
|
||||
}
|
||||
|
||||
function optionalString(value: string | null | undefined): string | null {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function dateFromMs(ms: bigint): Date {
|
||||
return new Date(Number(ms));
|
||||
}
|
||||
|
||||
function optionalDateFromMs(ms: bigint): Date | null {
|
||||
if (ms === 0n) {
|
||||
return null;
|
||||
}
|
||||
return new Date(Number(ms));
|
||||
}
|
||||
@@ -1,27 +1,207 @@
|
||||
<!--
|
||||
Phase 10 stub for the diplomatic-mail active view. Phase 28 wires the
|
||||
real mail listing.
|
||||
Phase 28 active-view for the diplomatic mail. Replaces the Phase 10
|
||||
stub. Renders a two-pane list/detail layout on desktop and a
|
||||
one-pane stack on mobile; the inner pieces (thread list, thread
|
||||
pane, system-item pane, compose form) live under
|
||||
`./mail/*.svelte`.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import { mailStore, type MailListEntry } from "$lib/mail-store.svelte";
|
||||
|
||||
import ThreadList from "./mail/thread-list.svelte";
|
||||
import ThreadPane from "./mail/thread-pane.svelte";
|
||||
import SystemItemPane from "./mail/system-item-pane.svelte";
|
||||
import Compose from "./mail/compose.svelte";
|
||||
|
||||
let selectedKey = $state<string | null>(null);
|
||||
let composeOpen = $state(false);
|
||||
|
||||
const gameId = $derived(page.params.id ?? "");
|
||||
|
||||
const entries = $derived(mailStore.entries);
|
||||
|
||||
const selected = $derived.by<MailListEntry | null>(() => {
|
||||
if (selectedKey === null) {
|
||||
return null;
|
||||
}
|
||||
return entries.find((entry) => entryKey(entry) === selectedKey) ?? null;
|
||||
});
|
||||
|
||||
function entryKey(entry: MailListEntry): string {
|
||||
return entry.kind === "thread"
|
||||
? `thread:${entry.raceName}`
|
||||
: `standalone:${entry.message.messageId}`;
|
||||
}
|
||||
|
||||
function openEntry(entry: MailListEntry): void {
|
||||
selectedKey = entryKey(entry);
|
||||
}
|
||||
|
||||
function closePane(): void {
|
||||
selectedKey = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="active-view" data-testid="active-view-mail">
|
||||
<h2>{i18n.t("game.view.mail")}</h2>
|
||||
<p>{i18n.t("game.shell.coming_soon")}</p>
|
||||
<section class="mail" data-testid="active-view-mail">
|
||||
<header class="mail-header">
|
||||
<h2>{i18n.t("game.view.mail")}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="compose-btn"
|
||||
data-testid="mail-compose-open"
|
||||
onclick={() => (composeOpen = true)}
|
||||
disabled={mailStore.status !== "ready"}
|
||||
>
|
||||
{i18n.t("game.mail.compose_action")}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#if mailStore.status === "loading"}
|
||||
<p class="status" data-testid="mail-loading">
|
||||
{i18n.t("game.mail.loading")}
|
||||
</p>
|
||||
{:else if mailStore.status === "error"}
|
||||
<p class="status error" data-testid="mail-error">
|
||||
{mailStore.error ?? i18n.t("game.mail.load_failed")}
|
||||
</p>
|
||||
{:else if entries.length === 0}
|
||||
<p class="status" data-testid="mail-empty">
|
||||
{i18n.t("game.mail.empty")}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="panes" class:detail-open={selected !== null}>
|
||||
<div class="list-pane">
|
||||
<ThreadList
|
||||
{entries}
|
||||
selectedKey={selectedKey}
|
||||
onSelect={openEntry}
|
||||
/>
|
||||
</div>
|
||||
<div class="detail-pane">
|
||||
<button
|
||||
type="button"
|
||||
class="back-btn"
|
||||
data-testid="mail-back"
|
||||
onclick={closePane}
|
||||
>
|
||||
{i18n.t("game.mail.back")}
|
||||
</button>
|
||||
{#if selected === null}
|
||||
<p class="status empty-detail">
|
||||
{i18n.t("game.mail.select_thread")}
|
||||
</p>
|
||||
{:else if selected.kind === "thread"}
|
||||
<ThreadPane thread={selected} {gameId} />
|
||||
{:else}
|
||||
<SystemItemPane entry={selected} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if composeOpen}
|
||||
<Compose
|
||||
onClose={() => (composeOpen = false)}
|
||||
onSent={(raceName: string | null) => {
|
||||
composeOpen = false;
|
||||
if (raceName !== null) {
|
||||
selectedKey = `thread:${raceName}`;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.active-view {
|
||||
padding: 1.5rem;
|
||||
.mail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
.active-view h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
.mail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.mail-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.active-view p {
|
||||
margin: 0;
|
||||
color: #555;
|
||||
.compose-btn {
|
||||
font: inherit;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid #444;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.compose-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.status {
|
||||
color: #888;
|
||||
}
|
||||
.status.error {
|
||||
color: #c62828;
|
||||
}
|
||||
.panes {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 280px) 1fr;
|
||||
gap: 1rem;
|
||||
min-height: 320px;
|
||||
}
|
||||
.list-pane,
|
||||
.detail-pane {
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
background: #111;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-pane {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.back-btn {
|
||||
display: none;
|
||||
font: inherit;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid #444;
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.empty-detail {
|
||||
text-align: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.panes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.list-pane {
|
||||
display: block;
|
||||
}
|
||||
.detail-pane {
|
||||
display: none;
|
||||
}
|
||||
.panes.detail-open .list-pane {
|
||||
display: none;
|
||||
}
|
||||
.panes.detail-open .detail-pane {
|
||||
display: block;
|
||||
}
|
||||
.panes.detail-open .back-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
<!--
|
||||
Phase 28 — compose dialog for diplomatic mail. The recipient picker
|
||||
reads `gameState.report.races[]` (Phase 22), the kind toggle exposes
|
||||
personal / broadcast / admin; broadcast and admin sends are gated
|
||||
server-side, the UI surfaces the resulting 403 inline.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import { mailStore } from "$lib/mail-store.svelte";
|
||||
import {
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
type RenderedReportSource,
|
||||
} from "$lib/rendered-report.svelte";
|
||||
|
||||
type ComposeKind = "personal" | "broadcast" | "admin";
|
||||
type AdminAudience = "active" | "active_and_removed" | "all_members";
|
||||
|
||||
let {
|
||||
onClose,
|
||||
onSent,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSent: (raceName: string | null) => void;
|
||||
} = $props();
|
||||
|
||||
const rendered = getContext<RenderedReportSource | undefined>(
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
);
|
||||
|
||||
const races = $derived.by<string[]>(() => {
|
||||
const r = rendered?.report;
|
||||
if (!r) {
|
||||
return [];
|
||||
}
|
||||
return r.races.map((race) => race.name);
|
||||
});
|
||||
|
||||
let kind = $state<ComposeKind>("personal");
|
||||
let raceName = $state("");
|
||||
let adminTarget = $state<"user" | "all">("user");
|
||||
let adminAudience = $state<AdminAudience>("active");
|
||||
let subject = $state("");
|
||||
let body = $state("");
|
||||
let error = $state<string | null>(null);
|
||||
let sending = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (raceName === "" && races.length > 0) {
|
||||
raceName = races[0];
|
||||
}
|
||||
});
|
||||
|
||||
async function submit(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
error = null;
|
||||
const bodyText = body.trim();
|
||||
if (bodyText === "") {
|
||||
error = i18n.t("game.mail.body_required");
|
||||
return;
|
||||
}
|
||||
const needsRecipient = kind === "personal" || (kind === "admin" && adminTarget === "user");
|
||||
if (needsRecipient && raceName === "") {
|
||||
error = i18n.t("game.mail.recipient_required");
|
||||
return;
|
||||
}
|
||||
sending = true;
|
||||
try {
|
||||
if (kind === "personal") {
|
||||
await mailStore.composePersonal({
|
||||
raceName,
|
||||
subject,
|
||||
body: bodyText,
|
||||
});
|
||||
onSent(raceName);
|
||||
return;
|
||||
}
|
||||
if (kind === "broadcast") {
|
||||
await mailStore.composeBroadcast({ subject, body: bodyText });
|
||||
onSent(null);
|
||||
return;
|
||||
}
|
||||
await mailStore.composeAdmin({
|
||||
target: adminTarget,
|
||||
raceName: adminTarget === "user" ? raceName : undefined,
|
||||
recipients: adminTarget === "all" ? adminAudience : undefined,
|
||||
subject,
|
||||
body: bodyText,
|
||||
});
|
||||
onSent(null);
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
sending = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overlay" data-testid="mail-compose">
|
||||
<form class="dialog" onsubmit={submit}>
|
||||
<header>
|
||||
<h3>{i18n.t("game.mail.compose_action")}</h3>
|
||||
<button type="button" class="close" onclick={onClose}>×</button>
|
||||
</header>
|
||||
|
||||
<label>
|
||||
{i18n.t("game.mail.compose.target_label")}
|
||||
<select bind:value={kind} data-testid="mail-compose-kind">
|
||||
<option value="personal">{i18n.t("game.mail.compose.target_personal")}</option>
|
||||
<option value="broadcast">{i18n.t("game.mail.compose.target_broadcast")}</option>
|
||||
<option value="admin">{i18n.t("game.mail.compose.target_admin")}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{#if kind === "admin"}
|
||||
<label>
|
||||
{i18n.t("game.mail.compose.recipients_label")}
|
||||
<select bind:value={adminTarget} data-testid="mail-compose-admin-target">
|
||||
<option value="user">{i18n.t("game.mail.compose.target_personal")}</option>
|
||||
<option value="all">{i18n.t("game.mail.compose.target_broadcast")}</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if adminTarget === "all"}
|
||||
<label>
|
||||
{i18n.t("game.mail.compose.recipients_label")}
|
||||
<select bind:value={adminAudience} data-testid="mail-compose-admin-audience">
|
||||
<option value="active">{i18n.t("game.mail.compose.recipients_active")}</option>
|
||||
<option value="active_and_removed">{i18n.t("game.mail.compose.recipients_active_and_removed")}</option>
|
||||
<option value="all_members">{i18n.t("game.mail.compose.recipients_all_members")}</option>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if kind === "personal" || (kind === "admin" && adminTarget === "user")}
|
||||
<label>
|
||||
{i18n.t("game.mail.recipient_label")}
|
||||
<select bind:value={raceName} data-testid="mail-compose-recipient">
|
||||
{#each races as race (race)}
|
||||
<option value={race}>{race}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<label>
|
||||
<span class="visually-hidden">{i18n.t("game.mail.subject_placeholder")}</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={subject}
|
||||
placeholder={i18n.t("game.mail.subject_placeholder")}
|
||||
data-testid="mail-compose-subject"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span class="visually-hidden">{i18n.t("game.mail.body_placeholder")}</span>
|
||||
<textarea
|
||||
bind:value={body}
|
||||
placeholder={i18n.t("game.mail.body_placeholder")}
|
||||
rows="6"
|
||||
data-testid="mail-compose-body"
|
||||
></textarea>
|
||||
</label>
|
||||
|
||||
{#if error}
|
||||
<p class="error" data-testid="mail-compose-error">{error}</p>
|
||||
{/if}
|
||||
|
||||
<footer>
|
||||
<button type="button" onclick={onClose}>
|
||||
{i18n.t("game.mail.compose.cancel")}
|
||||
</button>
|
||||
<button type="submit" disabled={sending} data-testid="mail-compose-send">
|
||||
{i18n.t("game.mail.compose.send")}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 50;
|
||||
}
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #161616;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
min-width: min(420px, 90vw);
|
||||
max-width: min(560px, 95vw);
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.close {
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
font-size: 0.85rem;
|
||||
color: #ccc;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
padding: 0.4rem 0.5rem;
|
||||
border: 1px solid #444;
|
||||
background: #111;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
footer button {
|
||||
font: inherit;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid #444;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
footer button[type="submit"] {
|
||||
background: #2a4d7d;
|
||||
border-color: #2a4d7d;
|
||||
}
|
||||
footer button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
color: #c62828;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!--
|
||||
Phase 28 — right-pane for stand-alone messages (system mail, admin
|
||||
notifications, and the caller's own paid-tier broadcasts). The pane
|
||||
is read-only: no reply box, no per-recipient context. Soft-delete is
|
||||
available for incoming rows that the caller has read.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import { mailStore, type MailStandalone } from "$lib/mail-store.svelte";
|
||||
import { systemTitleKey } from "./system-titles";
|
||||
|
||||
let { entry }: { entry: MailStandalone } = $props();
|
||||
|
||||
let showOriginal = $state(false);
|
||||
|
||||
const incoming = $derived(entry.message.recipientUserName !== "" && entry.message.senderKind !== "player");
|
||||
|
||||
onMount(() => {
|
||||
if (incoming && entry.message.readAt === null) {
|
||||
void mailStore.markRead(entry.message.messageId);
|
||||
}
|
||||
});
|
||||
|
||||
const displayBody = $derived(
|
||||
entry.message.translatedBody && !showOriginal
|
||||
? entry.message.translatedBody
|
||||
: entry.message.body,
|
||||
);
|
||||
const displaySubject = $derived(
|
||||
entry.message.translatedSubject && !showOriginal
|
||||
? entry.message.translatedSubject
|
||||
: entry.message.subject,
|
||||
);
|
||||
|
||||
const headerKey = $derived.by(() => {
|
||||
const m = entry.message;
|
||||
if (m.senderKind === "system") {
|
||||
return systemTitleKey(m);
|
||||
}
|
||||
if (m.senderKind === "admin") {
|
||||
return "game.mail.admin.title" as const;
|
||||
}
|
||||
return "game.mail.broadcast.title" as const;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="standalone" data-testid="mail-system-item">
|
||||
<h3 class="title">{i18n.t(headerKey)}</h3>
|
||||
{#if displaySubject}
|
||||
<div class="subject">{displaySubject}</div>
|
||||
{/if}
|
||||
<p class="body">{displayBody}</p>
|
||||
{#if entry.message.translatedBody}
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
onclick={() => (showOriginal = !showOriginal)}
|
||||
>
|
||||
{showOriginal ? i18n.t("game.mail.show_translation") : i18n.t("game.mail.show_original")}
|
||||
</button>
|
||||
{/if}
|
||||
{#if incoming}
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
onclick={() => mailStore.softDelete(entry.message.messageId)}
|
||||
data-testid="mail-system-delete"
|
||||
>
|
||||
{i18n.t("game.mail.delete_action")}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.standalone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #b3a14c;
|
||||
}
|
||||
.subject {
|
||||
font-weight: 700;
|
||||
}
|
||||
.body {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.toggle,
|
||||
.delete {
|
||||
align-self: flex-start;
|
||||
font: inherit;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border: 1px solid #444;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
// Maps a system-mail message (lifecycle hook) to its i18n title key.
|
||||
// Kept as a typed helper so the thread-list and detail panes pick the
|
||||
// same title even when the body templates evolve.
|
||||
|
||||
import type { TranslationKey } from "$lib/i18n/index.svelte";
|
||||
import type { MailMessage } from "../../../api/diplomail";
|
||||
|
||||
const KEYWORDS: Array<{ test: RegExp; key: TranslationKey }> = [
|
||||
{ test: /game[._ ]paused/i, key: "game.mail.system.game_paused.title" },
|
||||
{ test: /game[._ ]cancelled|cancelled/i, key: "game.mail.system.game_cancelled.title" },
|
||||
{ test: /membership[._ ]removed|kicked/i, key: "game.mail.system.membership_removed.title" },
|
||||
{ test: /membership[._ ]blocked|blocked/i, key: "game.mail.system.membership_blocked.title" },
|
||||
];
|
||||
|
||||
/**
|
||||
* systemTitleKey returns the localised title key for a system mail
|
||||
* row. The lobby renders these messages through templated subjects;
|
||||
* the UI matches on the subject to pick a canonical title regardless
|
||||
* of language. Falls back to a generic system-mail title when no
|
||||
* pattern matches.
|
||||
*/
|
||||
export function systemTitleKey(message: MailMessage): TranslationKey {
|
||||
const subject = message.subject ?? "";
|
||||
for (const { test, key } of KEYWORDS) {
|
||||
if (test.test(subject)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return "game.mail.system.generic.title";
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<!--
|
||||
Phase 28 — left-pane list of mail entries. Each entry is either a
|
||||
per-race thread (collapsed to the latest message) or a stand-alone
|
||||
system / admin / outgoing-broadcast item. The list is virtual only
|
||||
inside its scroll container; PixiJS / canvas concerns do not apply
|
||||
here.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import type { MailListEntry } from "$lib/mail-store.svelte";
|
||||
import { systemTitleKey } from "./system-titles";
|
||||
|
||||
let {
|
||||
entries,
|
||||
selectedKey,
|
||||
onSelect,
|
||||
}: {
|
||||
entries: MailListEntry[];
|
||||
selectedKey: string | null;
|
||||
onSelect: (entry: MailListEntry) => void;
|
||||
} = $props();
|
||||
|
||||
function entryKey(entry: MailListEntry): string {
|
||||
return entry.kind === "thread"
|
||||
? `thread:${entry.raceName}`
|
||||
: `standalone:${entry.message.messageId}`;
|
||||
}
|
||||
|
||||
function snippet(entry: MailListEntry): string {
|
||||
if (entry.kind === "thread") {
|
||||
const last = entry.messages[entry.messages.length - 1];
|
||||
return last.subject || last.body;
|
||||
}
|
||||
return entry.message.subject || entry.message.body;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="list" data-testid="mail-thread-list">
|
||||
{#each entries as entry (entryKey(entry))}
|
||||
<li
|
||||
class="row"
|
||||
class:active={selectedKey === entryKey(entry)}
|
||||
class:standalone={entry.kind === "standalone"}
|
||||
class:has-unread={entry.kind === "thread" && entry.unreadCount > 0}
|
||||
data-testid="mail-list-row"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
onclick={() => onSelect(entry)}
|
||||
data-thread-key={entryKey(entry)}
|
||||
>
|
||||
<span class="title">
|
||||
{#if entry.kind === "thread"}
|
||||
{entry.raceName}
|
||||
{:else if entry.message.senderKind === "system"}
|
||||
{i18n.t(systemTitleKey(entry.message))}
|
||||
{:else if entry.message.senderKind === "admin"}
|
||||
{i18n.t("game.mail.admin.title")}
|
||||
{:else}
|
||||
{i18n.t("game.mail.broadcast.title")}
|
||||
{/if}
|
||||
</span>
|
||||
{#if entry.kind === "thread" && entry.unreadCount > 0}
|
||||
<span class="badge" data-testid="mail-row-unread">{entry.unreadCount}</span>
|
||||
{/if}
|
||||
<span class="snippet">{snippet(entry)}</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.row-btn {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 0.25rem 0.5rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.row.active .row-btn {
|
||||
border-color: #555;
|
||||
background: #1c1c1c;
|
||||
}
|
||||
.row.has-unread .title {
|
||||
font-weight: 700;
|
||||
}
|
||||
.row.standalone .title {
|
||||
color: #b3a14c;
|
||||
}
|
||||
.title {
|
||||
grid-column: 1 / span 1;
|
||||
}
|
||||
.badge {
|
||||
grid-column: 2 / span 1;
|
||||
grid-row: 1 / span 1;
|
||||
justify-self: end;
|
||||
min-width: 1.5rem;
|
||||
padding: 0 0.4rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: #2a4d7d;
|
||||
color: #fff;
|
||||
}
|
||||
.snippet {
|
||||
grid-column: 1 / span 2;
|
||||
color: #999;
|
||||
font-size: 0.85rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!--
|
||||
Phase 28 — right-pane transcript for a single per-race thread.
|
||||
Renders messages oldest → newest, with outgoing messages visually
|
||||
distinct from incoming. Each message body goes through `textContent`
|
||||
(no HTML parsing); the optional translation has a per-message
|
||||
"show original" / "show translation" toggle. A persistent reply box
|
||||
sits at the bottom of the pane.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import { mailStore, type MailThread } from "$lib/mail-store.svelte";
|
||||
import type { MailMessage } from "../../../api/diplomail";
|
||||
|
||||
let {
|
||||
thread,
|
||||
gameId,
|
||||
}: {
|
||||
thread: MailThread;
|
||||
gameId: string;
|
||||
} = $props();
|
||||
|
||||
let replyBody = $state("");
|
||||
let replyError = $state<string | null>(null);
|
||||
let sending = $state(false);
|
||||
const showOriginal = $state<Map<string, boolean>>(new Map());
|
||||
|
||||
// Mark every still-unread incoming message in this thread as read
|
||||
// when the pane mounts. Idempotent on the server; the store
|
||||
// optimistically flips `readAt` so the header badge updates
|
||||
// without waiting for the round-trip.
|
||||
onMount(() => {
|
||||
for (const m of thread.messages) {
|
||||
const incoming = m.senderRaceName === thread.raceName;
|
||||
if (incoming && m.readAt === null) {
|
||||
void mailStore.markRead(m.messageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function isOutgoing(m: MailMessage): boolean {
|
||||
// Outgoing messages have the local user as sender, which
|
||||
// corresponds to `recipientRaceName === thread.raceName` (the
|
||||
// thread is keyed on the other party's race name).
|
||||
return m.recipientRaceName === thread.raceName;
|
||||
}
|
||||
|
||||
function displayBody(m: MailMessage): string {
|
||||
if (m.translatedBody && !showOriginal.get(m.messageId)) {
|
||||
return m.translatedBody;
|
||||
}
|
||||
return m.body;
|
||||
}
|
||||
|
||||
function displaySubject(m: MailMessage): string {
|
||||
if (m.translatedSubject && !showOriginal.get(m.messageId)) {
|
||||
return m.translatedSubject ?? "";
|
||||
}
|
||||
return m.subject;
|
||||
}
|
||||
|
||||
function toggleTranslation(messageId: string): void {
|
||||
showOriginal.set(messageId, !(showOriginal.get(messageId) ?? false));
|
||||
// Trigger reactivity on the map proxy.
|
||||
showOriginal.size; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
||||
}
|
||||
|
||||
async function submitReply(event: SubmitEvent): Promise<void> {
|
||||
event.preventDefault();
|
||||
replyError = null;
|
||||
const body = replyBody.trim();
|
||||
if (body === "") {
|
||||
replyError = i18n.t("game.mail.body_required");
|
||||
return;
|
||||
}
|
||||
sending = true;
|
||||
try {
|
||||
await mailStore.composePersonal({
|
||||
raceName: thread.raceName,
|
||||
subject: "",
|
||||
body,
|
||||
});
|
||||
replyBody = "";
|
||||
} catch (err) {
|
||||
replyError = err instanceof Error ? err.message : String(err);
|
||||
} finally {
|
||||
sending = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Force the component to depend on the gameId rune so a game
|
||||
// switch re-mounts the pane (the parent unmounts the old one
|
||||
// implicitly via the entry-key change, but this keeps the
|
||||
// dependency explicit for SSR-disabled hot reloads).
|
||||
void gameId;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="thread" data-testid="mail-thread-pane">
|
||||
<h3 class="title">{thread.raceName}</h3>
|
||||
<ol class="messages">
|
||||
{#each thread.messages as m (m.messageId)}
|
||||
<li class="message" class:outgoing={isOutgoing(m)}>
|
||||
<div class="meta">
|
||||
<span class="from">
|
||||
{#if isOutgoing(m)}
|
||||
{i18n.t("game.mail.outgoing_label")}
|
||||
{:else}
|
||||
{thread.raceName}
|
||||
{/if}
|
||||
</span>
|
||||
<time>{m.createdAt.toISOString().slice(0, 19).replace("T", " ")}</time>
|
||||
</div>
|
||||
{#if displaySubject(m)}
|
||||
<div class="subject">{displaySubject(m)}</div>
|
||||
{/if}
|
||||
<p class="body">{displayBody(m)}</p>
|
||||
{#if m.translatedBody}
|
||||
<button
|
||||
type="button"
|
||||
class="toggle"
|
||||
onclick={() => toggleTranslation(m.messageId)}
|
||||
>
|
||||
{#if showOriginal.get(m.messageId)}
|
||||
{i18n.t("game.mail.show_translation")}
|
||||
{:else}
|
||||
{i18n.t("game.mail.show_original")}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isOutgoing(m)}
|
||||
<button
|
||||
type="button"
|
||||
class="delete"
|
||||
onclick={() => mailStore.softDelete(m.messageId)}
|
||||
data-testid="mail-delete"
|
||||
>
|
||||
{i18n.t("game.mail.delete_action")}
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<form class="reply" onsubmit={submitReply}>
|
||||
<label for="mail-reply-body">{i18n.t("game.mail.reply_label")}</label>
|
||||
<textarea
|
||||
id="mail-reply-body"
|
||||
bind:value={replyBody}
|
||||
placeholder={i18n.t("game.mail.body_placeholder")}
|
||||
rows="3"
|
||||
data-testid="mail-reply-body"
|
||||
></textarea>
|
||||
{#if replyError}
|
||||
<p class="error" data-testid="mail-reply-error">{replyError}</p>
|
||||
{/if}
|
||||
<button type="submit" disabled={sending} data-testid="mail-reply-send">
|
||||
{i18n.t("game.mail.compose.send")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.thread {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
height: 100%;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.messages {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
}
|
||||
.message {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: #1c1c1c;
|
||||
border: 1px solid #2a2a2a;
|
||||
}
|
||||
.message.outgoing {
|
||||
background: #15252e;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8rem;
|
||||
color: #999;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.subject {
|
||||
font-weight: 700;
|
||||
}
|
||||
.body {
|
||||
margin: 0.25rem 0 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.toggle,
|
||||
.delete {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
font: inherit;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border: 1px solid #444;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.reply {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.reply textarea {
|
||||
font: inherit;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #444;
|
||||
background: #111;
|
||||
color: inherit;
|
||||
border-radius: 4px;
|
||||
resize: vertical;
|
||||
}
|
||||
.reply button {
|
||||
align-self: flex-end;
|
||||
font: inherit;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid #444;
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.reply button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error {
|
||||
color: #c62828;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -15,10 +15,13 @@ polishes microcopy.
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
|
||||
import { mailStore } from "$lib/mail-store.svelte";
|
||||
|
||||
type Props = { gameId: string };
|
||||
let { gameId }: Props = $props();
|
||||
|
||||
const mailUnread = $derived(mailStore.unreadCount);
|
||||
|
||||
let open = $state(false);
|
||||
let rootEl: HTMLDivElement | null = $state(null);
|
||||
|
||||
@@ -122,9 +125,15 @@ polishes microcopy.
|
||||
type="button"
|
||||
role="menuitem"
|
||||
data-testid="view-menu-item-mail"
|
||||
class="with-badge"
|
||||
onclick={() => go(`/games/${gameId}/mail`)}
|
||||
>
|
||||
{i18n.t("game.view.mail")}
|
||||
<span>{i18n.t("game.view.mail")}</span>
|
||||
{#if mailUnread > 0}
|
||||
<span class="badge" data-testid="view-menu-item-mail-badge">
|
||||
{i18n.t("game.view.mail.badge", { count: String(mailUnread) })}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -200,6 +209,21 @@ polishes microcopy.
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.surface > button.with-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.surface > button.with-badge .badge {
|
||||
min-width: 1.5rem;
|
||||
padding: 0 0.4rem;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: #2a4d7d;
|
||||
color: #fff;
|
||||
}
|
||||
.surface > button:hover,
|
||||
.surface > details > summary:hover {
|
||||
background: #1c2238;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -124,6 +124,46 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.view.report": "отчёт хода",
|
||||
"game.view.battle": "журнал боёв",
|
||||
"game.view.mail": "дипломатическая почта",
|
||||
"game.view.mail.badge": "{count}",
|
||||
"game.events.mail_new.message": "новое письмо от {from}",
|
||||
"game.events.mail_new.action": "открыть",
|
||||
"game.mail.loading": "загрузка почты…",
|
||||
"game.mail.load_failed": "не удалось загрузить почту",
|
||||
"game.mail.empty": "дипломатических сообщений пока нет",
|
||||
"game.mail.back": "назад",
|
||||
"game.mail.compose_action": "написать",
|
||||
"game.mail.select_thread": "выбери ветку слева",
|
||||
"game.mail.broadcast.title": "твоя рассылка",
|
||||
"game.mail.admin.title": "административное уведомление",
|
||||
"game.mail.system.generic.title": "системное сообщение",
|
||||
"game.mail.system.game_paused.title": "игра поставлена на паузу",
|
||||
"game.mail.system.game_cancelled.title": "игра отменена",
|
||||
"game.mail.system.membership_removed.title": "членство удалено",
|
||||
"game.mail.system.membership_blocked.title": "членство заблокировано",
|
||||
"game.mail.subject_placeholder": "тема (необязательно)",
|
||||
"game.mail.body_placeholder": "твоё сообщение…",
|
||||
"game.mail.recipient_label": "раса",
|
||||
"game.mail.recipient_required": "выбери расу-получателя",
|
||||
"game.mail.body_required": "тело сообщения не может быть пустым",
|
||||
"game.mail.body_too_long": "длина тела превышает лимит {limit} байт",
|
||||
"game.mail.subject_too_long": "длина темы превышает лимит {limit} байт",
|
||||
"game.mail.compose.send": "отправить",
|
||||
"game.mail.compose.cancel": "отмена",
|
||||
"game.mail.compose.target_personal": "личное",
|
||||
"game.mail.compose.target_broadcast": "рассылка",
|
||||
"game.mail.compose.target_admin": "админ.",
|
||||
"game.mail.compose.recipients_active": "активным членам",
|
||||
"game.mail.compose.recipients_active_and_removed": "активным + удалённым",
|
||||
"game.mail.compose.recipients_all_members": "всем членам",
|
||||
"game.mail.compose.target_label": "тип",
|
||||
"game.mail.compose.recipients_label": "адресаты",
|
||||
"game.mail.compose.send_failed": "отправка не удалась",
|
||||
"game.mail.show_original": "показать оригинал",
|
||||
"game.mail.show_translation": "показать перевод",
|
||||
"game.mail.translation_unavailable": "перевод недоступен",
|
||||
"game.mail.reply_label": "ответить",
|
||||
"game.mail.delete_action": "удалить",
|
||||
"game.mail.outgoing_label": "ты",
|
||||
"game.view.designer.ship_class": "конструктор класса кораблей",
|
||||
"game.view.designer.science": "редактор наук",
|
||||
"game.sidebar.tab.calculator": "калькулятор",
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
// Phase 28 reactive store for the in-game diplomatic-mail view. Owns
|
||||
// the inbox / sent listings, the per-race threading projection, the
|
||||
// unread badge counter, and the imperative compose / mark-read /
|
||||
// delete actions. The companion API wrappers live in
|
||||
// `src/api/diplomail.ts`; this store coordinates them with the rest
|
||||
// of the in-game shell.
|
||||
|
||||
import type { GalaxyClient } from "../api/galaxy-client";
|
||||
import type { Cache } from "../platform/store/index";
|
||||
import {
|
||||
deleteMessage,
|
||||
fetchInbox,
|
||||
fetchMessage,
|
||||
fetchSent,
|
||||
markRead,
|
||||
sendAdmin,
|
||||
sendBroadcast,
|
||||
sendPersonal,
|
||||
type MailMessage,
|
||||
type SendAdminArgs,
|
||||
type SendBroadcastArgs,
|
||||
type SendPersonalArgs,
|
||||
} from "../api/diplomail";
|
||||
|
||||
/**
|
||||
* MailThread groups personal messages exchanged with a single other
|
||||
* race into one entry. The local player's outgoing messages live
|
||||
* alongside incoming messages from the same race so the UI renders a
|
||||
* chat-style transcript. `unreadCount` counts only incoming messages
|
||||
* with `readAt === null`.
|
||||
*/
|
||||
export interface MailThread {
|
||||
kind: "thread";
|
||||
raceName: string;
|
||||
messages: MailMessage[];
|
||||
unreadCount: number;
|
||||
latestAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* MailStandalone wraps a single message that does not participate in
|
||||
* a race-thread: system mail, admin notifications, and the caller's
|
||||
* own paid-tier broadcasts. The UI renders these as read-only items
|
||||
* in the same list as the per-race threads.
|
||||
*/
|
||||
export interface MailStandalone {
|
||||
kind: "standalone";
|
||||
message: MailMessage;
|
||||
latestAt: Date;
|
||||
}
|
||||
|
||||
export type MailListEntry = MailThread | MailStandalone;
|
||||
|
||||
const CACHE_NAMESPACE = "diplomail";
|
||||
|
||||
/**
|
||||
* MailStore is the reactive surface consumed by the active view, the
|
||||
* header badge, and the push-event handler. One instance per signed-
|
||||
* in session is enough — the rune fields are scoped to the current
|
||||
* game and replaced on every `setGame` call so navigating between
|
||||
* games stays clean.
|
||||
*/
|
||||
export class MailStore {
|
||||
gameId = $state("");
|
||||
status: "idle" | "loading" | "ready" | "error" = $state("idle");
|
||||
error: string | null = $state(null);
|
||||
inbox: MailMessage[] = $state([]);
|
||||
sent: MailMessage[] = $state([]);
|
||||
|
||||
private client: GalaxyClient | null = null;
|
||||
private cache: Cache | null = null;
|
||||
|
||||
/**
|
||||
* entries surfaces the unified list-pane projection: per-race
|
||||
* threads built from incoming + outgoing personal messages plus
|
||||
* stand-alone items for system / admin / own-broadcast rows.
|
||||
* Sorted newest-first by the latest message inside each entry.
|
||||
*/
|
||||
entries: MailListEntry[] = $derived.by(() => buildEntries(this.inbox, this.sent));
|
||||
|
||||
/**
|
||||
* unreadCount drives the header view-menu badge. Counts only
|
||||
* incoming personal / admin / system messages with `readAt === null`.
|
||||
* `read_at` is not surfaced to the user in the UI but still
|
||||
* drives this counter.
|
||||
*/
|
||||
unreadCount = $derived.by(() => this.inbox.reduce((acc, m) => (m.readAt === null ? acc + 1 : acc), 0));
|
||||
|
||||
/**
|
||||
* init configures the dependencies and fires the initial fetch.
|
||||
* Safe to call multiple times — calls after the first one are
|
||||
* routed to `setGame`. `localUserId` is captured so the threading
|
||||
* projection can tell outgoing messages from incoming when the
|
||||
* inbox and sent lists are unified.
|
||||
*/
|
||||
async init(opts: {
|
||||
client: GalaxyClient;
|
||||
cache: Cache;
|
||||
gameId: string;
|
||||
}): Promise<void> {
|
||||
this.client = opts.client;
|
||||
this.cache = opts.cache;
|
||||
await this.setGame(opts.gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* setGame switches the store to a different game id and refreshes
|
||||
* its inbox / sent state. Idempotent on the same id — the network
|
||||
* fetch fires only when the id actually changed or the previous
|
||||
* load ended in `error`.
|
||||
*/
|
||||
async setGame(gameId: string): Promise<void> {
|
||||
if (this.client === null) {
|
||||
throw new Error("mail-store: setGame called before init");
|
||||
}
|
||||
if (this.gameId === gameId && this.status === "ready") {
|
||||
return;
|
||||
}
|
||||
this.gameId = gameId;
|
||||
this.status = "loading";
|
||||
this.error = null;
|
||||
this.inbox = [];
|
||||
this.sent = [];
|
||||
try {
|
||||
const [inbox, sent] = await Promise.all([
|
||||
fetchInbox(this.client, gameId),
|
||||
fetchSent(this.client, gameId),
|
||||
]);
|
||||
this.inbox = inbox;
|
||||
this.sent = sent;
|
||||
this.status = "ready";
|
||||
await this.rememberLastSeen();
|
||||
} catch (err) {
|
||||
this.status = "error";
|
||||
this.error = errorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** refresh re-fetches inbox + sent for the active game. */
|
||||
async refresh(): Promise<void> {
|
||||
if (this.gameId === "") {
|
||||
return;
|
||||
}
|
||||
await this.setGame(this.gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* applyPushEvent reacts to a verified `diplomail.message.received`
|
||||
* push frame by refetching the inbox for the active game. The
|
||||
* payload carries only a preview, so the store hits the server for
|
||||
* the canonical row.
|
||||
*/
|
||||
async applyPushEvent(payloadGameId: string): Promise<void> {
|
||||
if (payloadGameId !== this.gameId || this.client === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.inbox = await fetchInbox(this.client, this.gameId);
|
||||
await this.rememberLastSeen();
|
||||
} catch (err) {
|
||||
this.error = errorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* markRead transitions an incoming message to `read`. The local
|
||||
* inbox row is flipped optimistically; on failure the previous
|
||||
* state is restored and the error surfaces via `error`.
|
||||
*/
|
||||
async markRead(messageId: string): Promise<void> {
|
||||
if (this.client === null) {
|
||||
return;
|
||||
}
|
||||
const before = this.inbox;
|
||||
this.inbox = before.map((m) => {
|
||||
if (m.messageId !== messageId) {
|
||||
return m;
|
||||
}
|
||||
return { ...m, readAt: m.readAt ?? new Date() };
|
||||
});
|
||||
try {
|
||||
await markRead(this.client, this.gameId, messageId);
|
||||
} catch (err) {
|
||||
this.inbox = before;
|
||||
this.error = errorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* softDelete removes a read incoming message from the inbox. The
|
||||
* server enforces "read before delete"; on conflict the row is
|
||||
* restored and the error surfaces.
|
||||
*/
|
||||
async softDelete(messageId: string): Promise<void> {
|
||||
if (this.client === null) {
|
||||
return;
|
||||
}
|
||||
const before = this.inbox;
|
||||
this.inbox = before.filter((m) => m.messageId !== messageId);
|
||||
try {
|
||||
await deleteMessage(this.client, this.gameId, messageId);
|
||||
} catch (err) {
|
||||
this.inbox = before;
|
||||
this.error = errorMessage(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* composePersonal sends a single-recipient personal message,
|
||||
* addressed by race name (resolved server-side). On success the
|
||||
* resulting row is appended to the sent list so the matching
|
||||
* thread surfaces it immediately. Throws on failure so callers
|
||||
* can render inline form errors.
|
||||
*/
|
||||
async composePersonal(input: Omit<SendPersonalArgs, "gameId">): Promise<MailMessage> {
|
||||
if (this.client === null) {
|
||||
throw new Error("mail-store: composePersonal called before init");
|
||||
}
|
||||
const created = await sendPersonal(this.client, { ...input, gameId: this.gameId });
|
||||
this.sent = [created, ...this.sent];
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* composeBroadcast posts a paid-tier player broadcast. The sent
|
||||
* list is refreshed to surface the new entries.
|
||||
*/
|
||||
async composeBroadcast(input: Omit<SendBroadcastArgs, "gameId">): Promise<void> {
|
||||
if (this.client === null) {
|
||||
throw new Error("mail-store: composeBroadcast called before init");
|
||||
}
|
||||
await sendBroadcast(this.client, { ...input, gameId: this.gameId });
|
||||
this.sent = await fetchSent(this.client, this.gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* composeAdmin posts an owner-only admin notification. Single
|
||||
* sends refresh the sent list; broadcasts also refresh the sent
|
||||
* list (the author does not appear as a recipient and is excluded
|
||||
* from the resulting fan-out).
|
||||
*/
|
||||
async composeAdmin(input: Omit<SendAdminArgs, "gameId">): Promise<void> {
|
||||
if (this.client === null) {
|
||||
throw new Error("mail-store: composeAdmin called before init");
|
||||
}
|
||||
await sendAdmin(this.client, { ...input, gameId: this.gameId });
|
||||
this.sent = await fetchSent(this.client, this.gameId);
|
||||
}
|
||||
|
||||
/**
|
||||
* loadMessage fetches a single message detail (used when the UI
|
||||
* needs the freshest translation status for a specific row).
|
||||
* The returned row is merged into the inbox copy if it lives
|
||||
* there; sent rows are not refreshed here.
|
||||
*/
|
||||
async loadMessage(messageId: string): Promise<MailMessage | null> {
|
||||
if (this.client === null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const fresh = await fetchMessage(this.client, this.gameId, messageId);
|
||||
this.inbox = this.inbox.map((m) => (m.messageId === messageId ? fresh : m));
|
||||
return fresh;
|
||||
} catch (err) {
|
||||
this.error = errorMessage(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async rememberLastSeen(): Promise<void> {
|
||||
if (this.cache === null || this.gameId === "" || this.inbox.length === 0) {
|
||||
return;
|
||||
}
|
||||
const last = this.inbox[0];
|
||||
await this.cache.put(CACHE_NAMESPACE, `${this.gameId}/last-seen`, last.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
function buildEntries(inbox: MailMessage[], sent: MailMessage[]): MailListEntry[] {
|
||||
// Each personal message keyed by another race contributes to a
|
||||
// race thread. Other shapes become stand-alone entries.
|
||||
const threadsByRace = new Map<string, MailThread>();
|
||||
const standalones: MailStandalone[] = [];
|
||||
|
||||
for (const m of inbox) {
|
||||
if (isStandaloneIncoming(m)) {
|
||||
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
|
||||
continue;
|
||||
}
|
||||
const race = m.senderRaceName ?? "";
|
||||
if (race === "") {
|
||||
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
|
||||
continue;
|
||||
}
|
||||
mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/true);
|
||||
}
|
||||
|
||||
for (const m of sent) {
|
||||
if (isStandaloneOutgoing(m)) {
|
||||
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
|
||||
continue;
|
||||
}
|
||||
const race = m.recipientRaceName ?? "";
|
||||
if (race === "") {
|
||||
standalones.push({ kind: "standalone", message: m, latestAt: m.createdAt });
|
||||
continue;
|
||||
}
|
||||
mergeIntoThread(threadsByRace, race, m, /*isIncoming=*/false);
|
||||
}
|
||||
|
||||
// Sort each thread's messages oldest → newest for chat-style
|
||||
// rendering; the entry list itself sorts newest-first by the
|
||||
// most-recent message timestamp.
|
||||
for (const thread of threadsByRace.values()) {
|
||||
thread.messages.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||
const last = thread.messages[thread.messages.length - 1];
|
||||
thread.latestAt = last.createdAt;
|
||||
}
|
||||
|
||||
const entries: MailListEntry[] = [
|
||||
...Array.from(threadsByRace.values()),
|
||||
...standalones,
|
||||
];
|
||||
entries.sort((a, b) => b.latestAt.getTime() - a.latestAt.getTime());
|
||||
return entries;
|
||||
}
|
||||
|
||||
function mergeIntoThread(
|
||||
threads: Map<string, MailThread>,
|
||||
race: string,
|
||||
message: MailMessage,
|
||||
isIncoming: boolean,
|
||||
): void {
|
||||
let thread = threads.get(race);
|
||||
if (thread === undefined) {
|
||||
thread = {
|
||||
kind: "thread",
|
||||
raceName: race,
|
||||
messages: [],
|
||||
unreadCount: 0,
|
||||
latestAt: message.createdAt,
|
||||
};
|
||||
threads.set(race, thread);
|
||||
}
|
||||
thread.messages.push(message);
|
||||
if (isIncoming && message.readAt === null) {
|
||||
thread.unreadCount += 1;
|
||||
}
|
||||
if (message.createdAt.getTime() > thread.latestAt.getTime()) {
|
||||
thread.latestAt = message.createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
function isStandaloneIncoming(m: MailMessage): boolean {
|
||||
// System / admin notifications never thread by race even when a
|
||||
// snapshot is available — they are one-way operational mail.
|
||||
return m.senderKind !== "player";
|
||||
}
|
||||
|
||||
function isStandaloneOutgoing(m: MailMessage): boolean {
|
||||
// Paid-tier broadcasts that the caller authored target many
|
||||
// recipients; the UI renders them once as a stand-alone item.
|
||||
return m.broadcastScope !== "single";
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown): string {
|
||||
if (err instanceof Error) {
|
||||
return err.message;
|
||||
}
|
||||
return String(err);
|
||||
}
|
||||
|
||||
export const mailStore = new MailStore();
|
||||
@@ -0,0 +1,23 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
export { AdminRequest, AdminRequestT } from './diplomail/admin-request.js';
|
||||
export { AdminResponse, AdminResponseT } from './diplomail/admin-response.js';
|
||||
export { BroadcastRequest, BroadcastRequestT } from './diplomail/broadcast-request.js';
|
||||
export { BroadcastResponse, BroadcastResponseT } from './diplomail/broadcast-response.js';
|
||||
export { DeleteRequest, DeleteRequestT } from './diplomail/delete-request.js';
|
||||
export { DeleteResponse, DeleteResponseT } from './diplomail/delete-response.js';
|
||||
export { InboxRequest, InboxRequestT } from './diplomail/inbox-request.js';
|
||||
export { InboxResponse, InboxResponseT } from './diplomail/inbox-response.js';
|
||||
export { MailBroadcastReceipt, MailBroadcastReceiptT } from './diplomail/mail-broadcast-receipt.js';
|
||||
export { MailMessage, MailMessageT } from './diplomail/mail-message.js';
|
||||
export { MailRecipientState, MailRecipientStateT } from './diplomail/mail-recipient-state.js';
|
||||
export { MessageGetRequest, MessageGetRequestT } from './diplomail/message-get-request.js';
|
||||
export { MessageGetResponse, MessageGetResponseT } from './diplomail/message-get-response.js';
|
||||
export { ReadRequest, ReadRequestT } from './diplomail/read-request.js';
|
||||
export { ReadResponse, ReadResponseT } from './diplomail/read-response.js';
|
||||
export { SendRequest, SendRequestT } from './diplomail/send-request.js';
|
||||
export { SendResponse, SendResponseT } from './diplomail/send-response.js';
|
||||
export { SentRequest, SentRequestT } from './diplomail/sent-request.js';
|
||||
export { SentResponse, SentResponseT } from './diplomail/sent-response.js';
|
||||
@@ -0,0 +1,179 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class AdminRequest implements flatbuffers.IUnpackableObject<AdminRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):AdminRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsAdminRequest(bb:flatbuffers.ByteBuffer, obj?:AdminRequest):AdminRequest {
|
||||
return (obj || new AdminRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsAdminRequest(bb:flatbuffers.ByteBuffer, obj?:AdminRequest):AdminRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new AdminRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
target():string|null
|
||||
target(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
target(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipientUserId():string|null
|
||||
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipientRaceName():string|null
|
||||
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipients():string|null
|
||||
recipients(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipients(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 12);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
subject():string|null
|
||||
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
subject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 14);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
body():string|null
|
||||
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
body(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 16);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startAdminRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(7);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addTarget(builder:flatbuffers.Builder, targetOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, targetOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, recipientUserIdOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(3, recipientRaceNameOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipients(builder:flatbuffers.Builder, recipientsOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(4, recipientsOffset, 0);
|
||||
}
|
||||
|
||||
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(5, subjectOffset, 0);
|
||||
}
|
||||
|
||||
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(6, bodyOffset, 0);
|
||||
}
|
||||
|
||||
static endAdminRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createAdminRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, targetOffset:flatbuffers.Offset, recipientUserIdOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, recipientsOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
AdminRequest.startAdminRequest(builder);
|
||||
AdminRequest.addGameId(builder, gameIdOffset);
|
||||
AdminRequest.addTarget(builder, targetOffset);
|
||||
AdminRequest.addRecipientUserId(builder, recipientUserIdOffset);
|
||||
AdminRequest.addRecipientRaceName(builder, recipientRaceNameOffset);
|
||||
AdminRequest.addRecipients(builder, recipientsOffset);
|
||||
AdminRequest.addSubject(builder, subjectOffset);
|
||||
AdminRequest.addBody(builder, bodyOffset);
|
||||
return AdminRequest.endAdminRequest(builder);
|
||||
}
|
||||
|
||||
unpack(): AdminRequestT {
|
||||
return new AdminRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
this.target(),
|
||||
this.recipientUserId(),
|
||||
this.recipientRaceName(),
|
||||
this.recipients(),
|
||||
this.subject(),
|
||||
this.body()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: AdminRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.target = this.target();
|
||||
_o.recipientUserId = this.recipientUserId();
|
||||
_o.recipientRaceName = this.recipientRaceName();
|
||||
_o.recipients = this.recipients();
|
||||
_o.subject = this.subject();
|
||||
_o.body = this.body();
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public target: string|Uint8Array|null = null,
|
||||
public recipientUserId: string|Uint8Array|null = null,
|
||||
public recipientRaceName: string|Uint8Array|null = null,
|
||||
public recipients: string|Uint8Array|null = null,
|
||||
public subject: string|Uint8Array|null = null,
|
||||
public body: string|Uint8Array|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const target = (this.target !== null ? builder.createString(this.target!) : 0);
|
||||
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
|
||||
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
|
||||
const recipients = (this.recipients !== null ? builder.createString(this.recipients!) : 0);
|
||||
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
|
||||
const body = (this.body !== null ? builder.createString(this.body!) : 0);
|
||||
|
||||
return AdminRequest.createAdminRequest(builder,
|
||||
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
|
||||
target,
|
||||
recipientUserId,
|
||||
recipientRaceName,
|
||||
recipients,
|
||||
subject,
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailBroadcastReceipt, MailBroadcastReceiptT } from '../diplomail/mail-broadcast-receipt.js';
|
||||
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
|
||||
|
||||
|
||||
export class AdminResponse implements flatbuffers.IUnpackableObject<AdminResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):AdminResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsAdminResponse(bb:flatbuffers.ByteBuffer, obj?:AdminResponse):AdminResponse {
|
||||
return (obj || new AdminResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsAdminResponse(bb:flatbuffers.ByteBuffer, obj?:AdminResponse):AdminResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new AdminResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
message(obj?:MailMessage):MailMessage|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
receipt(obj?:MailBroadcastReceipt):MailBroadcastReceipt|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? (obj || new MailBroadcastReceipt()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startAdminResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageOffset, 0);
|
||||
}
|
||||
|
||||
static addReceipt(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, receiptOffset, 0);
|
||||
}
|
||||
|
||||
static endAdminResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
|
||||
unpack(): AdminResponseT {
|
||||
return new AdminResponseT(
|
||||
(this.message() !== null ? this.message()!.unpack() : null),
|
||||
(this.receipt() !== null ? this.receipt()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: AdminResponseT): void {
|
||||
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
|
||||
_o.receipt = (this.receipt() !== null ? this.receipt()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public message: MailMessageT|null = null,
|
||||
public receipt: MailBroadcastReceiptT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const message = (this.message !== null ? this.message!.pack(builder) : 0);
|
||||
const receipt = (this.receipt !== null ? this.receipt!.pack(builder) : 0);
|
||||
|
||||
AdminResponse.startAdminResponse(builder);
|
||||
AdminResponse.addMessage(builder, message);
|
||||
AdminResponse.addReceipt(builder, receipt);
|
||||
|
||||
return AdminResponse.endAdminResponse(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class BroadcastRequest implements flatbuffers.IUnpackableObject<BroadcastRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):BroadcastRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsBroadcastRequest(bb:flatbuffers.ByteBuffer, obj?:BroadcastRequest):BroadcastRequest {
|
||||
return (obj || new BroadcastRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsBroadcastRequest(bb:flatbuffers.ByteBuffer, obj?:BroadcastRequest):BroadcastRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new BroadcastRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
subject():string|null
|
||||
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
subject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
body():string|null
|
||||
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
body(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startBroadcastRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(3);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, subjectOffset, 0);
|
||||
}
|
||||
|
||||
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, bodyOffset, 0);
|
||||
}
|
||||
|
||||
static endBroadcastRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createBroadcastRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
BroadcastRequest.startBroadcastRequest(builder);
|
||||
BroadcastRequest.addGameId(builder, gameIdOffset);
|
||||
BroadcastRequest.addSubject(builder, subjectOffset);
|
||||
BroadcastRequest.addBody(builder, bodyOffset);
|
||||
return BroadcastRequest.endBroadcastRequest(builder);
|
||||
}
|
||||
|
||||
unpack(): BroadcastRequestT {
|
||||
return new BroadcastRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
this.subject(),
|
||||
this.body()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: BroadcastRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.subject = this.subject();
|
||||
_o.body = this.body();
|
||||
}
|
||||
}
|
||||
|
||||
export class BroadcastRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public subject: string|Uint8Array|null = null,
|
||||
public body: string|Uint8Array|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
|
||||
const body = (this.body !== null ? builder.createString(this.body!) : 0);
|
||||
|
||||
return BroadcastRequest.createBroadcastRequest(builder,
|
||||
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
|
||||
subject,
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailBroadcastReceipt, MailBroadcastReceiptT } from '../diplomail/mail-broadcast-receipt.js';
|
||||
|
||||
|
||||
export class BroadcastResponse implements flatbuffers.IUnpackableObject<BroadcastResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):BroadcastResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsBroadcastResponse(bb:flatbuffers.ByteBuffer, obj?:BroadcastResponse):BroadcastResponse {
|
||||
return (obj || new BroadcastResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsBroadcastResponse(bb:flatbuffers.ByteBuffer, obj?:BroadcastResponse):BroadcastResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new BroadcastResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
receipt(obj?:MailBroadcastReceipt):MailBroadcastReceipt|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailBroadcastReceipt()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startBroadcastResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addReceipt(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, receiptOffset, 0);
|
||||
}
|
||||
|
||||
static endBroadcastResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createBroadcastResponse(builder:flatbuffers.Builder, receiptOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
BroadcastResponse.startBroadcastResponse(builder);
|
||||
BroadcastResponse.addReceipt(builder, receiptOffset);
|
||||
return BroadcastResponse.endBroadcastResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): BroadcastResponseT {
|
||||
return new BroadcastResponseT(
|
||||
(this.receipt() !== null ? this.receipt()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: BroadcastResponseT): void {
|
||||
_o.receipt = (this.receipt() !== null ? this.receipt()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class BroadcastResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public receipt: MailBroadcastReceiptT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const receipt = (this.receipt !== null ? this.receipt!.pack(builder) : 0);
|
||||
|
||||
return BroadcastResponse.createBroadcastResponse(builder,
|
||||
receipt
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class DeleteRequest implements flatbuffers.IUnpackableObject<DeleteRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):DeleteRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsDeleteRequest(bb:flatbuffers.ByteBuffer, obj?:DeleteRequest):DeleteRequest {
|
||||
return (obj || new DeleteRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsDeleteRequest(bb:flatbuffers.ByteBuffer, obj?:DeleteRequest):DeleteRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new DeleteRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
messageId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
static startDeleteRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(1, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static endDeleteRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
builder.requiredField(offset, 6) // message_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
|
||||
unpack(): DeleteRequestT {
|
||||
return new DeleteRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
(this.messageId() !== null ? this.messageId()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: DeleteRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public messageId: UUIDT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
DeleteRequest.startDeleteRequest(builder);
|
||||
DeleteRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
|
||||
DeleteRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
|
||||
|
||||
return DeleteRequest.endDeleteRequest(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailRecipientState, MailRecipientStateT } from '../diplomail/mail-recipient-state.js';
|
||||
|
||||
|
||||
export class DeleteResponse implements flatbuffers.IUnpackableObject<DeleteResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):DeleteResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsDeleteResponse(bb:flatbuffers.ByteBuffer, obj?:DeleteResponse):DeleteResponse {
|
||||
return (obj || new DeleteResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsDeleteResponse(bb:flatbuffers.ByteBuffer, obj?:DeleteResponse):DeleteResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new DeleteResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
state(obj?:MailRecipientState):MailRecipientState|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailRecipientState()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startDeleteResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addState(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, stateOffset, 0);
|
||||
}
|
||||
|
||||
static endDeleteResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createDeleteResponse(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
DeleteResponse.startDeleteResponse(builder);
|
||||
DeleteResponse.addState(builder, stateOffset);
|
||||
return DeleteResponse.endDeleteResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): DeleteResponseT {
|
||||
return new DeleteResponseT(
|
||||
(this.state() !== null ? this.state()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: DeleteResponseT): void {
|
||||
_o.state = (this.state() !== null ? this.state()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public state: MailRecipientStateT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const state = (this.state !== null ? this.state!.pack(builder) : 0);
|
||||
|
||||
return DeleteResponse.createDeleteResponse(builder,
|
||||
state
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class InboxRequest implements flatbuffers.IUnpackableObject<InboxRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):InboxRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsInboxRequest(bb:flatbuffers.ByteBuffer, obj?:InboxRequest):InboxRequest {
|
||||
return (obj || new InboxRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsInboxRequest(bb:flatbuffers.ByteBuffer, obj?:InboxRequest):InboxRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new InboxRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
static startInboxRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static endInboxRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createInboxRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
InboxRequest.startInboxRequest(builder);
|
||||
InboxRequest.addGameId(builder, gameIdOffset);
|
||||
return InboxRequest.endInboxRequest(builder);
|
||||
}
|
||||
|
||||
unpack(): InboxRequestT {
|
||||
return new InboxRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: InboxRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class InboxRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
return InboxRequest.createInboxRequest(builder,
|
||||
(this.gameId !== null ? this.gameId!.pack(builder) : 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
|
||||
|
||||
|
||||
export class InboxResponse implements flatbuffers.IUnpackableObject<InboxResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):InboxResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsInboxResponse(bb:flatbuffers.ByteBuffer, obj?:InboxResponse):InboxResponse {
|
||||
return (obj || new InboxResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsInboxResponse(bb:flatbuffers.ByteBuffer, obj?:InboxResponse):InboxResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new InboxResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
items(index: number, obj?:MailMessage):MailMessage|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
|
||||
}
|
||||
|
||||
itemsLength():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startInboxResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addItems(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, itemsOffset, 0);
|
||||
}
|
||||
|
||||
static createItemsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
|
||||
builder.startVector(4, data.length, 4);
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
builder.addOffset(data[i]!);
|
||||
}
|
||||
return builder.endVector();
|
||||
}
|
||||
|
||||
static startItemsVector(builder:flatbuffers.Builder, numElems:number) {
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static endInboxResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createInboxResponse(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
InboxResponse.startInboxResponse(builder);
|
||||
InboxResponse.addItems(builder, itemsOffset);
|
||||
return InboxResponse.endInboxResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): InboxResponseT {
|
||||
return new InboxResponseT(
|
||||
this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: InboxResponseT): void {
|
||||
_o.items = this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength());
|
||||
}
|
||||
}
|
||||
|
||||
export class InboxResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public items: (MailMessageT)[] = []
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const items = InboxResponse.createItemsVector(builder, builder.createObjectOffsetList(this.items));
|
||||
|
||||
return InboxResponse.createInboxResponse(builder,
|
||||
items
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
|
||||
|
||||
export class MailBroadcastReceipt implements flatbuffers.IUnpackableObject<MailBroadcastReceiptT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):MailBroadcastReceipt {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsMailBroadcastReceipt(bb:flatbuffers.ByteBuffer, obj?:MailBroadcastReceipt):MailBroadcastReceipt {
|
||||
return (obj || new MailBroadcastReceipt()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsMailBroadcastReceipt(bb:flatbuffers.ByteBuffer, obj?:MailBroadcastReceipt):MailBroadcastReceipt {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new MailBroadcastReceipt()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
messageId():string|null
|
||||
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
messageId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameId():string|null
|
||||
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameName():string|null
|
||||
gameName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
kind():string|null
|
||||
kind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
kind(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
senderKind():string|null
|
||||
senderKind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
senderKind(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 12);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
subject():string|null
|
||||
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
subject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 14);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
body():string|null
|
||||
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
body(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 16);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
bodyLang():string|null
|
||||
bodyLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
bodyLang(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 18);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
broadcastScope():string|null
|
||||
broadcastScope(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
broadcastScope(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 20);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
createdAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 22);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
recipientCount():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 24);
|
||||
return offset ? this.bb!.readInt32(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startMailBroadcastReceipt(builder:flatbuffers.Builder) {
|
||||
builder.startObject(11);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addGameName(builder:flatbuffers.Builder, gameNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, gameNameOffset, 0);
|
||||
}
|
||||
|
||||
static addKind(builder:flatbuffers.Builder, kindOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(3, kindOffset, 0);
|
||||
}
|
||||
|
||||
static addSenderKind(builder:flatbuffers.Builder, senderKindOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(4, senderKindOffset, 0);
|
||||
}
|
||||
|
||||
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(5, subjectOffset, 0);
|
||||
}
|
||||
|
||||
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(6, bodyOffset, 0);
|
||||
}
|
||||
|
||||
static addBodyLang(builder:flatbuffers.Builder, bodyLangOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(7, bodyLangOffset, 0);
|
||||
}
|
||||
|
||||
static addBroadcastScope(builder:flatbuffers.Builder, broadcastScopeOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(8, broadcastScopeOffset, 0);
|
||||
}
|
||||
|
||||
static addCreatedAtMs(builder:flatbuffers.Builder, createdAtMs:bigint) {
|
||||
builder.addFieldInt64(9, createdAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static addRecipientCount(builder:flatbuffers.Builder, recipientCount:number) {
|
||||
builder.addFieldInt32(10, recipientCount, 0);
|
||||
}
|
||||
|
||||
static endMailBroadcastReceipt(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createMailBroadcastReceipt(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, gameNameOffset:flatbuffers.Offset, kindOffset:flatbuffers.Offset, senderKindOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset, bodyLangOffset:flatbuffers.Offset, broadcastScopeOffset:flatbuffers.Offset, createdAtMs:bigint, recipientCount:number):flatbuffers.Offset {
|
||||
MailBroadcastReceipt.startMailBroadcastReceipt(builder);
|
||||
MailBroadcastReceipt.addMessageId(builder, messageIdOffset);
|
||||
MailBroadcastReceipt.addGameId(builder, gameIdOffset);
|
||||
MailBroadcastReceipt.addGameName(builder, gameNameOffset);
|
||||
MailBroadcastReceipt.addKind(builder, kindOffset);
|
||||
MailBroadcastReceipt.addSenderKind(builder, senderKindOffset);
|
||||
MailBroadcastReceipt.addSubject(builder, subjectOffset);
|
||||
MailBroadcastReceipt.addBody(builder, bodyOffset);
|
||||
MailBroadcastReceipt.addBodyLang(builder, bodyLangOffset);
|
||||
MailBroadcastReceipt.addBroadcastScope(builder, broadcastScopeOffset);
|
||||
MailBroadcastReceipt.addCreatedAtMs(builder, createdAtMs);
|
||||
MailBroadcastReceipt.addRecipientCount(builder, recipientCount);
|
||||
return MailBroadcastReceipt.endMailBroadcastReceipt(builder);
|
||||
}
|
||||
|
||||
unpack(): MailBroadcastReceiptT {
|
||||
return new MailBroadcastReceiptT(
|
||||
this.messageId(),
|
||||
this.gameId(),
|
||||
this.gameName(),
|
||||
this.kind(),
|
||||
this.senderKind(),
|
||||
this.subject(),
|
||||
this.body(),
|
||||
this.bodyLang(),
|
||||
this.broadcastScope(),
|
||||
this.createdAtMs(),
|
||||
this.recipientCount()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: MailBroadcastReceiptT): void {
|
||||
_o.messageId = this.messageId();
|
||||
_o.gameId = this.gameId();
|
||||
_o.gameName = this.gameName();
|
||||
_o.kind = this.kind();
|
||||
_o.senderKind = this.senderKind();
|
||||
_o.subject = this.subject();
|
||||
_o.body = this.body();
|
||||
_o.bodyLang = this.bodyLang();
|
||||
_o.broadcastScope = this.broadcastScope();
|
||||
_o.createdAtMs = this.createdAtMs();
|
||||
_o.recipientCount = this.recipientCount();
|
||||
}
|
||||
}
|
||||
|
||||
export class MailBroadcastReceiptT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public messageId: string|Uint8Array|null = null,
|
||||
public gameId: string|Uint8Array|null = null,
|
||||
public gameName: string|Uint8Array|null = null,
|
||||
public kind: string|Uint8Array|null = null,
|
||||
public senderKind: string|Uint8Array|null = null,
|
||||
public subject: string|Uint8Array|null = null,
|
||||
public body: string|Uint8Array|null = null,
|
||||
public bodyLang: string|Uint8Array|null = null,
|
||||
public broadcastScope: string|Uint8Array|null = null,
|
||||
public createdAtMs: bigint = BigInt('0'),
|
||||
public recipientCount: number = 0
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
|
||||
const gameId = (this.gameId !== null ? builder.createString(this.gameId!) : 0);
|
||||
const gameName = (this.gameName !== null ? builder.createString(this.gameName!) : 0);
|
||||
const kind = (this.kind !== null ? builder.createString(this.kind!) : 0);
|
||||
const senderKind = (this.senderKind !== null ? builder.createString(this.senderKind!) : 0);
|
||||
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
|
||||
const body = (this.body !== null ? builder.createString(this.body!) : 0);
|
||||
const bodyLang = (this.bodyLang !== null ? builder.createString(this.bodyLang!) : 0);
|
||||
const broadcastScope = (this.broadcastScope !== null ? builder.createString(this.broadcastScope!) : 0);
|
||||
|
||||
return MailBroadcastReceipt.createMailBroadcastReceipt(builder,
|
||||
messageId,
|
||||
gameId,
|
||||
gameName,
|
||||
kind,
|
||||
senderKind,
|
||||
subject,
|
||||
body,
|
||||
bodyLang,
|
||||
broadcastScope,
|
||||
this.createdAtMs,
|
||||
this.recipientCount
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
|
||||
|
||||
export class MailMessage implements flatbuffers.IUnpackableObject<MailMessageT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):MailMessage {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsMailMessage(bb:flatbuffers.ByteBuffer, obj?:MailMessage):MailMessage {
|
||||
return (obj || new MailMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsMailMessage(bb:flatbuffers.ByteBuffer, obj?:MailMessage):MailMessage {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new MailMessage()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
messageId():string|null
|
||||
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
messageId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameId():string|null
|
||||
gameId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
gameName():string|null
|
||||
gameName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
gameName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
kind():string|null
|
||||
kind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
kind(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
senderKind():string|null
|
||||
senderKind(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
senderKind(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 12);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
senderUserId():string|null
|
||||
senderUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
senderUserId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 14);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
senderUsername():string|null
|
||||
senderUsername(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
senderUsername(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 16);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
senderRaceName():string|null
|
||||
senderRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
senderRaceName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 18);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
subject():string|null
|
||||
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
subject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 20);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
body():string|null
|
||||
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
body(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 22);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
bodyLang():string|null
|
||||
bodyLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
bodyLang(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 24);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
broadcastScope():string|null
|
||||
broadcastScope(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
broadcastScope(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 26);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
createdAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 28);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
recipientUserId():string|null
|
||||
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 30);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipientUserName():string|null
|
||||
recipientUserName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientUserName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 32);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipientRaceName():string|null
|
||||
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 34);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
readAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 36);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
deletedAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 38);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
translatedSubject():string|null
|
||||
translatedSubject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
translatedSubject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 40);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
translatedBody():string|null
|
||||
translatedBody(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
translatedBody(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 42);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
translationLang():string|null
|
||||
translationLang(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
translationLang(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 44);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
translator():string|null
|
||||
translator(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
translator(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 46);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startMailMessage(builder:flatbuffers.Builder) {
|
||||
builder.startObject(22);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addGameName(builder:flatbuffers.Builder, gameNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, gameNameOffset, 0);
|
||||
}
|
||||
|
||||
static addKind(builder:flatbuffers.Builder, kindOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(3, kindOffset, 0);
|
||||
}
|
||||
|
||||
static addSenderKind(builder:flatbuffers.Builder, senderKindOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(4, senderKindOffset, 0);
|
||||
}
|
||||
|
||||
static addSenderUserId(builder:flatbuffers.Builder, senderUserIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(5, senderUserIdOffset, 0);
|
||||
}
|
||||
|
||||
static addSenderUsername(builder:flatbuffers.Builder, senderUsernameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(6, senderUsernameOffset, 0);
|
||||
}
|
||||
|
||||
static addSenderRaceName(builder:flatbuffers.Builder, senderRaceNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(7, senderRaceNameOffset, 0);
|
||||
}
|
||||
|
||||
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(8, subjectOffset, 0);
|
||||
}
|
||||
|
||||
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(9, bodyOffset, 0);
|
||||
}
|
||||
|
||||
static addBodyLang(builder:flatbuffers.Builder, bodyLangOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(10, bodyLangOffset, 0);
|
||||
}
|
||||
|
||||
static addBroadcastScope(builder:flatbuffers.Builder, broadcastScopeOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(11, broadcastScopeOffset, 0);
|
||||
}
|
||||
|
||||
static addCreatedAtMs(builder:flatbuffers.Builder, createdAtMs:bigint) {
|
||||
builder.addFieldInt64(12, createdAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(13, recipientUserIdOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientUserName(builder:flatbuffers.Builder, recipientUserNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(14, recipientUserNameOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(15, recipientRaceNameOffset, 0);
|
||||
}
|
||||
|
||||
static addReadAtMs(builder:flatbuffers.Builder, readAtMs:bigint) {
|
||||
builder.addFieldInt64(16, readAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static addDeletedAtMs(builder:flatbuffers.Builder, deletedAtMs:bigint) {
|
||||
builder.addFieldInt64(17, deletedAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static addTranslatedSubject(builder:flatbuffers.Builder, translatedSubjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(18, translatedSubjectOffset, 0);
|
||||
}
|
||||
|
||||
static addTranslatedBody(builder:flatbuffers.Builder, translatedBodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(19, translatedBodyOffset, 0);
|
||||
}
|
||||
|
||||
static addTranslationLang(builder:flatbuffers.Builder, translationLangOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(20, translationLangOffset, 0);
|
||||
}
|
||||
|
||||
static addTranslator(builder:flatbuffers.Builder, translatorOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(21, translatorOffset, 0);
|
||||
}
|
||||
|
||||
static endMailMessage(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createMailMessage(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, gameIdOffset:flatbuffers.Offset, gameNameOffset:flatbuffers.Offset, kindOffset:flatbuffers.Offset, senderKindOffset:flatbuffers.Offset, senderUserIdOffset:flatbuffers.Offset, senderUsernameOffset:flatbuffers.Offset, senderRaceNameOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset, bodyLangOffset:flatbuffers.Offset, broadcastScopeOffset:flatbuffers.Offset, createdAtMs:bigint, recipientUserIdOffset:flatbuffers.Offset, recipientUserNameOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, readAtMs:bigint, deletedAtMs:bigint, translatedSubjectOffset:flatbuffers.Offset, translatedBodyOffset:flatbuffers.Offset, translationLangOffset:flatbuffers.Offset, translatorOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
MailMessage.startMailMessage(builder);
|
||||
MailMessage.addMessageId(builder, messageIdOffset);
|
||||
MailMessage.addGameId(builder, gameIdOffset);
|
||||
MailMessage.addGameName(builder, gameNameOffset);
|
||||
MailMessage.addKind(builder, kindOffset);
|
||||
MailMessage.addSenderKind(builder, senderKindOffset);
|
||||
MailMessage.addSenderUserId(builder, senderUserIdOffset);
|
||||
MailMessage.addSenderUsername(builder, senderUsernameOffset);
|
||||
MailMessage.addSenderRaceName(builder, senderRaceNameOffset);
|
||||
MailMessage.addSubject(builder, subjectOffset);
|
||||
MailMessage.addBody(builder, bodyOffset);
|
||||
MailMessage.addBodyLang(builder, bodyLangOffset);
|
||||
MailMessage.addBroadcastScope(builder, broadcastScopeOffset);
|
||||
MailMessage.addCreatedAtMs(builder, createdAtMs);
|
||||
MailMessage.addRecipientUserId(builder, recipientUserIdOffset);
|
||||
MailMessage.addRecipientUserName(builder, recipientUserNameOffset);
|
||||
MailMessage.addRecipientRaceName(builder, recipientRaceNameOffset);
|
||||
MailMessage.addReadAtMs(builder, readAtMs);
|
||||
MailMessage.addDeletedAtMs(builder, deletedAtMs);
|
||||
MailMessage.addTranslatedSubject(builder, translatedSubjectOffset);
|
||||
MailMessage.addTranslatedBody(builder, translatedBodyOffset);
|
||||
MailMessage.addTranslationLang(builder, translationLangOffset);
|
||||
MailMessage.addTranslator(builder, translatorOffset);
|
||||
return MailMessage.endMailMessage(builder);
|
||||
}
|
||||
|
||||
unpack(): MailMessageT {
|
||||
return new MailMessageT(
|
||||
this.messageId(),
|
||||
this.gameId(),
|
||||
this.gameName(),
|
||||
this.kind(),
|
||||
this.senderKind(),
|
||||
this.senderUserId(),
|
||||
this.senderUsername(),
|
||||
this.senderRaceName(),
|
||||
this.subject(),
|
||||
this.body(),
|
||||
this.bodyLang(),
|
||||
this.broadcastScope(),
|
||||
this.createdAtMs(),
|
||||
this.recipientUserId(),
|
||||
this.recipientUserName(),
|
||||
this.recipientRaceName(),
|
||||
this.readAtMs(),
|
||||
this.deletedAtMs(),
|
||||
this.translatedSubject(),
|
||||
this.translatedBody(),
|
||||
this.translationLang(),
|
||||
this.translator()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: MailMessageT): void {
|
||||
_o.messageId = this.messageId();
|
||||
_o.gameId = this.gameId();
|
||||
_o.gameName = this.gameName();
|
||||
_o.kind = this.kind();
|
||||
_o.senderKind = this.senderKind();
|
||||
_o.senderUserId = this.senderUserId();
|
||||
_o.senderUsername = this.senderUsername();
|
||||
_o.senderRaceName = this.senderRaceName();
|
||||
_o.subject = this.subject();
|
||||
_o.body = this.body();
|
||||
_o.bodyLang = this.bodyLang();
|
||||
_o.broadcastScope = this.broadcastScope();
|
||||
_o.createdAtMs = this.createdAtMs();
|
||||
_o.recipientUserId = this.recipientUserId();
|
||||
_o.recipientUserName = this.recipientUserName();
|
||||
_o.recipientRaceName = this.recipientRaceName();
|
||||
_o.readAtMs = this.readAtMs();
|
||||
_o.deletedAtMs = this.deletedAtMs();
|
||||
_o.translatedSubject = this.translatedSubject();
|
||||
_o.translatedBody = this.translatedBody();
|
||||
_o.translationLang = this.translationLang();
|
||||
_o.translator = this.translator();
|
||||
}
|
||||
}
|
||||
|
||||
export class MailMessageT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public messageId: string|Uint8Array|null = null,
|
||||
public gameId: string|Uint8Array|null = null,
|
||||
public gameName: string|Uint8Array|null = null,
|
||||
public kind: string|Uint8Array|null = null,
|
||||
public senderKind: string|Uint8Array|null = null,
|
||||
public senderUserId: string|Uint8Array|null = null,
|
||||
public senderUsername: string|Uint8Array|null = null,
|
||||
public senderRaceName: string|Uint8Array|null = null,
|
||||
public subject: string|Uint8Array|null = null,
|
||||
public body: string|Uint8Array|null = null,
|
||||
public bodyLang: string|Uint8Array|null = null,
|
||||
public broadcastScope: string|Uint8Array|null = null,
|
||||
public createdAtMs: bigint = BigInt('0'),
|
||||
public recipientUserId: string|Uint8Array|null = null,
|
||||
public recipientUserName: string|Uint8Array|null = null,
|
||||
public recipientRaceName: string|Uint8Array|null = null,
|
||||
public readAtMs: bigint = BigInt('0'),
|
||||
public deletedAtMs: bigint = BigInt('0'),
|
||||
public translatedSubject: string|Uint8Array|null = null,
|
||||
public translatedBody: string|Uint8Array|null = null,
|
||||
public translationLang: string|Uint8Array|null = null,
|
||||
public translator: string|Uint8Array|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
|
||||
const gameId = (this.gameId !== null ? builder.createString(this.gameId!) : 0);
|
||||
const gameName = (this.gameName !== null ? builder.createString(this.gameName!) : 0);
|
||||
const kind = (this.kind !== null ? builder.createString(this.kind!) : 0);
|
||||
const senderKind = (this.senderKind !== null ? builder.createString(this.senderKind!) : 0);
|
||||
const senderUserId = (this.senderUserId !== null ? builder.createString(this.senderUserId!) : 0);
|
||||
const senderUsername = (this.senderUsername !== null ? builder.createString(this.senderUsername!) : 0);
|
||||
const senderRaceName = (this.senderRaceName !== null ? builder.createString(this.senderRaceName!) : 0);
|
||||
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
|
||||
const body = (this.body !== null ? builder.createString(this.body!) : 0);
|
||||
const bodyLang = (this.bodyLang !== null ? builder.createString(this.bodyLang!) : 0);
|
||||
const broadcastScope = (this.broadcastScope !== null ? builder.createString(this.broadcastScope!) : 0);
|
||||
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
|
||||
const recipientUserName = (this.recipientUserName !== null ? builder.createString(this.recipientUserName!) : 0);
|
||||
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
|
||||
const translatedSubject = (this.translatedSubject !== null ? builder.createString(this.translatedSubject!) : 0);
|
||||
const translatedBody = (this.translatedBody !== null ? builder.createString(this.translatedBody!) : 0);
|
||||
const translationLang = (this.translationLang !== null ? builder.createString(this.translationLang!) : 0);
|
||||
const translator = (this.translator !== null ? builder.createString(this.translator!) : 0);
|
||||
|
||||
return MailMessage.createMailMessage(builder,
|
||||
messageId,
|
||||
gameId,
|
||||
gameName,
|
||||
kind,
|
||||
senderKind,
|
||||
senderUserId,
|
||||
senderUsername,
|
||||
senderRaceName,
|
||||
subject,
|
||||
body,
|
||||
bodyLang,
|
||||
broadcastScope,
|
||||
this.createdAtMs,
|
||||
recipientUserId,
|
||||
recipientUserName,
|
||||
recipientRaceName,
|
||||
this.readAtMs,
|
||||
this.deletedAtMs,
|
||||
translatedSubject,
|
||||
translatedBody,
|
||||
translationLang,
|
||||
translator
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
|
||||
|
||||
export class MailRecipientState implements flatbuffers.IUnpackableObject<MailRecipientStateT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):MailRecipientState {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsMailRecipientState(bb:flatbuffers.ByteBuffer, obj?:MailRecipientState):MailRecipientState {
|
||||
return (obj || new MailRecipientState()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsMailRecipientState(bb:flatbuffers.ByteBuffer, obj?:MailRecipientState):MailRecipientState {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new MailRecipientState()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
messageId():string|null
|
||||
messageId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
messageId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
readAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
deletedAtMs():bigint {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.readInt64(this.bb_pos + offset) : BigInt('0');
|
||||
}
|
||||
|
||||
static startMailRecipientState(builder:flatbuffers.Builder) {
|
||||
builder.startObject(3);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static addReadAtMs(builder:flatbuffers.Builder, readAtMs:bigint) {
|
||||
builder.addFieldInt64(1, readAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static addDeletedAtMs(builder:flatbuffers.Builder, deletedAtMs:bigint) {
|
||||
builder.addFieldInt64(2, deletedAtMs, BigInt('0'));
|
||||
}
|
||||
|
||||
static endMailRecipientState(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createMailRecipientState(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset, readAtMs:bigint, deletedAtMs:bigint):flatbuffers.Offset {
|
||||
MailRecipientState.startMailRecipientState(builder);
|
||||
MailRecipientState.addMessageId(builder, messageIdOffset);
|
||||
MailRecipientState.addReadAtMs(builder, readAtMs);
|
||||
MailRecipientState.addDeletedAtMs(builder, deletedAtMs);
|
||||
return MailRecipientState.endMailRecipientState(builder);
|
||||
}
|
||||
|
||||
unpack(): MailRecipientStateT {
|
||||
return new MailRecipientStateT(
|
||||
this.messageId(),
|
||||
this.readAtMs(),
|
||||
this.deletedAtMs()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: MailRecipientStateT): void {
|
||||
_o.messageId = this.messageId();
|
||||
_o.readAtMs = this.readAtMs();
|
||||
_o.deletedAtMs = this.deletedAtMs();
|
||||
}
|
||||
}
|
||||
|
||||
export class MailRecipientStateT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public messageId: string|Uint8Array|null = null,
|
||||
public readAtMs: bigint = BigInt('0'),
|
||||
public deletedAtMs: bigint = BigInt('0')
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const messageId = (this.messageId !== null ? builder.createString(this.messageId!) : 0);
|
||||
|
||||
return MailRecipientState.createMailRecipientState(builder,
|
||||
messageId,
|
||||
this.readAtMs,
|
||||
this.deletedAtMs
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class MessageGetRequest implements flatbuffers.IUnpackableObject<MessageGetRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):MessageGetRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsMessageGetRequest(bb:flatbuffers.ByteBuffer, obj?:MessageGetRequest):MessageGetRequest {
|
||||
return (obj || new MessageGetRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsMessageGetRequest(bb:flatbuffers.ByteBuffer, obj?:MessageGetRequest):MessageGetRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new MessageGetRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
messageId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
static startMessageGetRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(1, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static endMessageGetRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
builder.requiredField(offset, 6) // message_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
|
||||
unpack(): MessageGetRequestT {
|
||||
return new MessageGetRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
(this.messageId() !== null ? this.messageId()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: MessageGetRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageGetRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public messageId: UUIDT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
MessageGetRequest.startMessageGetRequest(builder);
|
||||
MessageGetRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
|
||||
MessageGetRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
|
||||
|
||||
return MessageGetRequest.endMessageGetRequest(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
|
||||
|
||||
|
||||
export class MessageGetResponse implements flatbuffers.IUnpackableObject<MessageGetResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):MessageGetResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsMessageGetResponse(bb:flatbuffers.ByteBuffer, obj?:MessageGetResponse):MessageGetResponse {
|
||||
return (obj || new MessageGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsMessageGetResponse(bb:flatbuffers.ByteBuffer, obj?:MessageGetResponse):MessageGetResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new MessageGetResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
message(obj?:MailMessage):MailMessage|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startMessageGetResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageOffset, 0);
|
||||
}
|
||||
|
||||
static endMessageGetResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createMessageGetResponse(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
MessageGetResponse.startMessageGetResponse(builder);
|
||||
MessageGetResponse.addMessage(builder, messageOffset);
|
||||
return MessageGetResponse.endMessageGetResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): MessageGetResponseT {
|
||||
return new MessageGetResponseT(
|
||||
(this.message() !== null ? this.message()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: MessageGetResponseT): void {
|
||||
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageGetResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public message: MailMessageT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const message = (this.message !== null ? this.message!.pack(builder) : 0);
|
||||
|
||||
return MessageGetResponse.createMessageGetResponse(builder,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class ReadRequest implements flatbuffers.IUnpackableObject<ReadRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):ReadRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsReadRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRequest):ReadRequest {
|
||||
return (obj || new ReadRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsReadRequest(bb:flatbuffers.ByteBuffer, obj?:ReadRequest):ReadRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new ReadRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
messageId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
static startReadRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(2);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addMessageId(builder:flatbuffers.Builder, messageIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(1, messageIdOffset, 0);
|
||||
}
|
||||
|
||||
static endReadRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
builder.requiredField(offset, 6) // message_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
|
||||
unpack(): ReadRequestT {
|
||||
return new ReadRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
(this.messageId() !== null ? this.messageId()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: ReadRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.messageId = (this.messageId() !== null ? this.messageId()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReadRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public messageId: UUIDT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
ReadRequest.startReadRequest(builder);
|
||||
ReadRequest.addGameId(builder, (this.gameId !== null ? this.gameId!.pack(builder) : 0));
|
||||
ReadRequest.addMessageId(builder, (this.messageId !== null ? this.messageId!.pack(builder) : 0));
|
||||
|
||||
return ReadRequest.endReadRequest(builder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailRecipientState, MailRecipientStateT } from '../diplomail/mail-recipient-state.js';
|
||||
|
||||
|
||||
export class ReadResponse implements flatbuffers.IUnpackableObject<ReadResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):ReadResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsReadResponse(bb:flatbuffers.ByteBuffer, obj?:ReadResponse):ReadResponse {
|
||||
return (obj || new ReadResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsReadResponse(bb:flatbuffers.ByteBuffer, obj?:ReadResponse):ReadResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new ReadResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
state(obj?:MailRecipientState):MailRecipientState|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailRecipientState()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startReadResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addState(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, stateOffset, 0);
|
||||
}
|
||||
|
||||
static endReadResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createReadResponse(builder:flatbuffers.Builder, stateOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
ReadResponse.startReadResponse(builder);
|
||||
ReadResponse.addState(builder, stateOffset);
|
||||
return ReadResponse.endReadResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): ReadResponseT {
|
||||
return new ReadResponseT(
|
||||
(this.state() !== null ? this.state()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: ReadResponseT): void {
|
||||
_o.state = (this.state() !== null ? this.state()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class ReadResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public state: MailRecipientStateT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const state = (this.state !== null ? this.state!.pack(builder) : 0);
|
||||
|
||||
return ReadResponse.createReadResponse(builder,
|
||||
state
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class SendRequest implements flatbuffers.IUnpackableObject<SendRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):SendRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest {
|
||||
return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsSendRequest(bb:flatbuffers.ByteBuffer, obj?:SendRequest):SendRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new SendRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
recipientUserId():string|null
|
||||
recipientUserId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientUserId(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 6);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
recipientRaceName():string|null
|
||||
recipientRaceName(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
recipientRaceName(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 8);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
subject():string|null
|
||||
subject(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
subject(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 10);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
body():string|null
|
||||
body(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null
|
||||
body(optionalEncoding?:any):string|Uint8Array|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 12);
|
||||
return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null;
|
||||
}
|
||||
|
||||
static startSendRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(5);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientUserId(builder:flatbuffers.Builder, recipientUserIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(1, recipientUserIdOffset, 0);
|
||||
}
|
||||
|
||||
static addRecipientRaceName(builder:flatbuffers.Builder, recipientRaceNameOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(2, recipientRaceNameOffset, 0);
|
||||
}
|
||||
|
||||
static addSubject(builder:flatbuffers.Builder, subjectOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(3, subjectOffset, 0);
|
||||
}
|
||||
|
||||
static addBody(builder:flatbuffers.Builder, bodyOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(4, bodyOffset, 0);
|
||||
}
|
||||
|
||||
static endSendRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createSendRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset, recipientUserIdOffset:flatbuffers.Offset, recipientRaceNameOffset:flatbuffers.Offset, subjectOffset:flatbuffers.Offset, bodyOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
SendRequest.startSendRequest(builder);
|
||||
SendRequest.addGameId(builder, gameIdOffset);
|
||||
SendRequest.addRecipientUserId(builder, recipientUserIdOffset);
|
||||
SendRequest.addRecipientRaceName(builder, recipientRaceNameOffset);
|
||||
SendRequest.addSubject(builder, subjectOffset);
|
||||
SendRequest.addBody(builder, bodyOffset);
|
||||
return SendRequest.endSendRequest(builder);
|
||||
}
|
||||
|
||||
unpack(): SendRequestT {
|
||||
return new SendRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null),
|
||||
this.recipientUserId(),
|
||||
this.recipientRaceName(),
|
||||
this.subject(),
|
||||
this.body()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: SendRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
_o.recipientUserId = this.recipientUserId();
|
||||
_o.recipientRaceName = this.recipientRaceName();
|
||||
_o.subject = this.subject();
|
||||
_o.body = this.body();
|
||||
}
|
||||
}
|
||||
|
||||
export class SendRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null,
|
||||
public recipientUserId: string|Uint8Array|null = null,
|
||||
public recipientRaceName: string|Uint8Array|null = null,
|
||||
public subject: string|Uint8Array|null = null,
|
||||
public body: string|Uint8Array|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const recipientUserId = (this.recipientUserId !== null ? builder.createString(this.recipientUserId!) : 0);
|
||||
const recipientRaceName = (this.recipientRaceName !== null ? builder.createString(this.recipientRaceName!) : 0);
|
||||
const subject = (this.subject !== null ? builder.createString(this.subject!) : 0);
|
||||
const body = (this.body !== null ? builder.createString(this.body!) : 0);
|
||||
|
||||
return SendRequest.createSendRequest(builder,
|
||||
(this.gameId !== null ? this.gameId!.pack(builder) : 0),
|
||||
recipientUserId,
|
||||
recipientRaceName,
|
||||
subject,
|
||||
body
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
|
||||
|
||||
|
||||
export class SendResponse implements flatbuffers.IUnpackableObject<SendResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):SendResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse {
|
||||
return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsSendResponse(bb:flatbuffers.ByteBuffer, obj?:SendResponse):SendResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new SendResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
message(obj?:MailMessage):MailMessage|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb_pos + offset), this.bb!) : null;
|
||||
}
|
||||
|
||||
static startSendResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addMessage(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, messageOffset, 0);
|
||||
}
|
||||
|
||||
static endSendResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createSendResponse(builder:flatbuffers.Builder, messageOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
SendResponse.startSendResponse(builder);
|
||||
SendResponse.addMessage(builder, messageOffset);
|
||||
return SendResponse.endSendResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): SendResponseT {
|
||||
return new SendResponseT(
|
||||
(this.message() !== null ? this.message()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: SendResponseT): void {
|
||||
_o.message = (this.message() !== null ? this.message()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class SendResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public message: MailMessageT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const message = (this.message !== null ? this.message!.pack(builder) : 0);
|
||||
|
||||
return SendResponse.createSendResponse(builder,
|
||||
message
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { UUID, UUIDT } from '../common/uuid.js';
|
||||
|
||||
|
||||
export class SentRequest implements flatbuffers.IUnpackableObject<SentRequestT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):SentRequest {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest {
|
||||
return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsSentRequest(bb:flatbuffers.ByteBuffer, obj?:SentRequest):SentRequest {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new SentRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
gameId(obj?:UUID):UUID|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new UUID()).__init(this.bb_pos + offset, this.bb!) : null;
|
||||
}
|
||||
|
||||
static startSentRequest(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addGameId(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset) {
|
||||
builder.addFieldStruct(0, gameIdOffset, 0);
|
||||
}
|
||||
|
||||
static endSentRequest(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
builder.requiredField(offset, 4) // game_id
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createSentRequest(builder:flatbuffers.Builder, gameIdOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
SentRequest.startSentRequest(builder);
|
||||
SentRequest.addGameId(builder, gameIdOffset);
|
||||
return SentRequest.endSentRequest(builder);
|
||||
}
|
||||
|
||||
unpack(): SentRequestT {
|
||||
return new SentRequestT(
|
||||
(this.gameId() !== null ? this.gameId()!.unpack() : null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: SentRequestT): void {
|
||||
_o.gameId = (this.gameId() !== null ? this.gameId()!.unpack() : null);
|
||||
}
|
||||
}
|
||||
|
||||
export class SentRequestT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public gameId: UUIDT|null = null
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
return SentRequest.createSentRequest(builder,
|
||||
(this.gameId !== null ? this.gameId!.pack(builder) : 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// automatically generated by the FlatBuffers compiler, do not modify
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import * as flatbuffers from 'flatbuffers';
|
||||
|
||||
import { MailMessage, MailMessageT } from '../diplomail/mail-message.js';
|
||||
|
||||
|
||||
export class SentResponse implements flatbuffers.IUnpackableObject<SentResponseT> {
|
||||
bb: flatbuffers.ByteBuffer|null = null;
|
||||
bb_pos = 0;
|
||||
__init(i:number, bb:flatbuffers.ByteBuffer):SentResponse {
|
||||
this.bb_pos = i;
|
||||
this.bb = bb;
|
||||
return this;
|
||||
}
|
||||
|
||||
static getRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse {
|
||||
return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
static getSizePrefixedRootAsSentResponse(bb:flatbuffers.ByteBuffer, obj?:SentResponse):SentResponse {
|
||||
bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH);
|
||||
return (obj || new SentResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb);
|
||||
}
|
||||
|
||||
items(index: number, obj?:MailMessage):MailMessage|null {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? (obj || new MailMessage()).__init(this.bb!.__indirect(this.bb!.__vector(this.bb_pos + offset) + index * 4), this.bb!) : null;
|
||||
}
|
||||
|
||||
itemsLength():number {
|
||||
const offset = this.bb!.__offset(this.bb_pos, 4);
|
||||
return offset ? this.bb!.__vector_len(this.bb_pos + offset) : 0;
|
||||
}
|
||||
|
||||
static startSentResponse(builder:flatbuffers.Builder) {
|
||||
builder.startObject(1);
|
||||
}
|
||||
|
||||
static addItems(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset) {
|
||||
builder.addFieldOffset(0, itemsOffset, 0);
|
||||
}
|
||||
|
||||
static createItemsVector(builder:flatbuffers.Builder, data:flatbuffers.Offset[]):flatbuffers.Offset {
|
||||
builder.startVector(4, data.length, 4);
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
builder.addOffset(data[i]!);
|
||||
}
|
||||
return builder.endVector();
|
||||
}
|
||||
|
||||
static startItemsVector(builder:flatbuffers.Builder, numElems:number) {
|
||||
builder.startVector(4, numElems, 4);
|
||||
}
|
||||
|
||||
static endSentResponse(builder:flatbuffers.Builder):flatbuffers.Offset {
|
||||
const offset = builder.endObject();
|
||||
return offset;
|
||||
}
|
||||
|
||||
static createSentResponse(builder:flatbuffers.Builder, itemsOffset:flatbuffers.Offset):flatbuffers.Offset {
|
||||
SentResponse.startSentResponse(builder);
|
||||
SentResponse.addItems(builder, itemsOffset);
|
||||
return SentResponse.endSentResponse(builder);
|
||||
}
|
||||
|
||||
unpack(): SentResponseT {
|
||||
return new SentResponseT(
|
||||
this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
unpackTo(_o: SentResponseT): void {
|
||||
_o.items = this.bb!.createObjList<MailMessage, MailMessageT>(this.items.bind(this), this.itemsLength());
|
||||
}
|
||||
}
|
||||
|
||||
export class SentResponseT implements flatbuffers.IGeneratedObject {
|
||||
constructor(
|
||||
public items: (MailMessageT)[] = []
|
||||
){}
|
||||
|
||||
|
||||
pack(builder:flatbuffers.Builder): flatbuffers.Offset {
|
||||
const items = SentResponse.createItemsVector(builder, builder.createObjectOffsetList(this.items));
|
||||
|
||||
return SentResponse.createSentResponse(builder,
|
||||
items
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ fresh.
|
||||
type VerifiedEvent,
|
||||
} from "../../../api/events.svelte";
|
||||
import { toast } from "$lib/toast.svelte";
|
||||
import { mailStore } from "$lib/mail-store.svelte";
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
@@ -241,6 +242,7 @@ fresh.
|
||||
// `currentTurn` is known cannot misfire.
|
||||
let unsubTurnReady: (() => void) | null = null;
|
||||
let unsubGamePaused: (() => void) | null = null;
|
||||
let unsubMailReceived: (() => void) | null = null;
|
||||
const turnReadyDecoder = new TextDecoder("utf-8");
|
||||
|
||||
function parseTurnReadyPayload(
|
||||
@@ -268,6 +270,32 @@ fresh.
|
||||
}
|
||||
}
|
||||
|
||||
function parseMailReceivedPayload(
|
||||
event: VerifiedEvent,
|
||||
): { gameId: string; from: string } | null {
|
||||
try {
|
||||
const text = turnReadyDecoder.decode(event.payloadBytes);
|
||||
const json: unknown = JSON.parse(text);
|
||||
if (typeof json !== "object" || json === null) {
|
||||
return null;
|
||||
}
|
||||
const record = json as Record<string, unknown>;
|
||||
const eventGameId = record.game_id;
|
||||
if (typeof eventGameId !== "string") {
|
||||
return null;
|
||||
}
|
||||
const subject =
|
||||
typeof record.subject === "string" && record.subject !== ""
|
||||
? record.subject
|
||||
: typeof record.preview === "string"
|
||||
? record.preview
|
||||
: "";
|
||||
return { gameId: eventGameId, from: subject };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseGamePausedPayload(
|
||||
event: VerifiedEvent,
|
||||
): { gameId: string; reason: string } | null {
|
||||
@@ -408,9 +436,29 @@ fresh.
|
||||
}
|
||||
orderDraft.markPaused({ reason: parsed.reason });
|
||||
});
|
||||
unsubMailReceived = eventStream.on(
|
||||
"diplomail.message.received",
|
||||
(event) => {
|
||||
const parsed = parseMailReceivedPayload(event);
|
||||
if (parsed === null || parsed.gameId !== gameId) {
|
||||
return;
|
||||
}
|
||||
void mailStore.applyPushEvent(parsed.gameId);
|
||||
toast.show({
|
||||
messageKey: "game.events.mail_new.message",
|
||||
messageParams: { from: parsed.from },
|
||||
actionLabelKey: "game.events.mail_new.action",
|
||||
onAction: () => {
|
||||
void goto(`/games/${gameId}/mail`);
|
||||
},
|
||||
durationMs: 8000,
|
||||
});
|
||||
},
|
||||
);
|
||||
await Promise.all([
|
||||
gameState.init({ client, cache, gameId }),
|
||||
orderDraft.init({ cache, gameId }),
|
||||
mailStore.init({ client, cache, gameId }),
|
||||
]);
|
||||
galaxyClient.set(client);
|
||||
orderDraft.bindClient(client, {
|
||||
@@ -442,6 +490,10 @@ fresh.
|
||||
unsubGamePaused();
|
||||
unsubGamePaused = null;
|
||||
}
|
||||
if (unsubMailReceived !== null) {
|
||||
unsubMailReceived();
|
||||
unsubMailReceived = null;
|
||||
}
|
||||
gameState.dispose();
|
||||
orderDraft.dispose();
|
||||
selection.dispose();
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Phase 28 — MailStore threading projection tests. Exercises the
|
||||
// `entries` derived rune end-to-end with handcrafted inbox + sent
|
||||
// fixtures; the network surface (GalaxyClient) is left null since
|
||||
// these tests do not call init / setGame.
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
MailStore,
|
||||
type MailListEntry,
|
||||
type MailStandalone,
|
||||
type MailThread,
|
||||
} from "../src/lib/mail-store.svelte";
|
||||
import type { MailMessage } from "../src/api/diplomail";
|
||||
|
||||
function makeMessage(overrides: Partial<MailMessage>): MailMessage {
|
||||
return {
|
||||
messageId: overrides.messageId ?? crypto.randomUUID(),
|
||||
gameId: overrides.gameId ?? "00000000-0000-0000-0000-000000000001",
|
||||
gameName: overrides.gameName ?? "Test game",
|
||||
kind: overrides.kind ?? "personal",
|
||||
senderKind: overrides.senderKind ?? "player",
|
||||
senderUserId: overrides.senderUserId ?? null,
|
||||
senderUsername: overrides.senderUsername ?? null,
|
||||
senderRaceName: overrides.senderRaceName ?? null,
|
||||
subject: overrides.subject ?? "",
|
||||
body: overrides.body ?? "",
|
||||
bodyLang: overrides.bodyLang ?? "en",
|
||||
broadcastScope: overrides.broadcastScope ?? "single",
|
||||
createdAt: overrides.createdAt ?? new Date(0),
|
||||
recipientUserId:
|
||||
overrides.recipientUserId ?? "00000000-0000-0000-0000-000000000099",
|
||||
recipientUserName: overrides.recipientUserName ?? "self",
|
||||
recipientRaceName: overrides.recipientRaceName ?? null,
|
||||
readAt: overrides.readAt ?? null,
|
||||
deletedAt: overrides.deletedAt ?? null,
|
||||
translatedSubject: overrides.translatedSubject ?? null,
|
||||
translatedBody: overrides.translatedBody ?? null,
|
||||
translationLang: overrides.translationLang ?? null,
|
||||
translator: overrides.translator ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MailStore.entries", () => {
|
||||
it("groups inbox + sent messages into per-race threads", () => {
|
||||
const store = new MailStore();
|
||||
const greysIncoming = makeMessage({
|
||||
messageId: "m1",
|
||||
senderKind: "player",
|
||||
senderRaceName: "Greys",
|
||||
body: "hi",
|
||||
createdAt: new Date("2026-05-15T12:00:00Z"),
|
||||
});
|
||||
const greysOutgoing = makeMessage({
|
||||
messageId: "m2",
|
||||
senderKind: "player",
|
||||
senderRaceName: "Self",
|
||||
recipientRaceName: "Greys",
|
||||
body: "reply",
|
||||
createdAt: new Date("2026-05-15T12:05:00Z"),
|
||||
});
|
||||
store.inbox = [greysIncoming];
|
||||
store.sent = [greysOutgoing];
|
||||
|
||||
const entries = store.entries;
|
||||
expect(entries.length).toBe(1);
|
||||
const entry = entries[0];
|
||||
expect(entry.kind).toBe("thread");
|
||||
const thread = entry as MailThread;
|
||||
expect(thread.raceName).toBe("Greys");
|
||||
expect(thread.messages.map((m) => m.messageId)).toEqual(["m1", "m2"]);
|
||||
});
|
||||
|
||||
it("surfaces system and admin messages as stand-alone items", () => {
|
||||
const store = new MailStore();
|
||||
const system = makeMessage({
|
||||
messageId: "sys-1",
|
||||
kind: "admin",
|
||||
senderKind: "system",
|
||||
subject: "game.paused: details",
|
||||
createdAt: new Date("2026-05-15T13:00:00Z"),
|
||||
});
|
||||
const admin = makeMessage({
|
||||
messageId: "adm-1",
|
||||
kind: "admin",
|
||||
senderKind: "admin",
|
||||
createdAt: new Date("2026-05-15T13:05:00Z"),
|
||||
});
|
||||
store.inbox = [system, admin];
|
||||
|
||||
const entries: MailListEntry[] = store.entries;
|
||||
expect(entries.every((e) => e.kind === "standalone")).toBe(true);
|
||||
const standalones = entries as MailStandalone[];
|
||||
expect(standalones.map((e) => e.message.messageId).sort()).toEqual(
|
||||
["adm-1", "sys-1"],
|
||||
);
|
||||
});
|
||||
|
||||
it("treats the local player's broadcasts as stand-alone outgoing", () => {
|
||||
const store = new MailStore();
|
||||
const broadcast = makeMessage({
|
||||
messageId: "br-1",
|
||||
senderKind: "player",
|
||||
senderRaceName: "Self",
|
||||
recipientRaceName: "Greys",
|
||||
broadcastScope: "game_broadcast",
|
||||
createdAt: new Date("2026-05-15T14:00:00Z"),
|
||||
});
|
||||
store.sent = [broadcast];
|
||||
|
||||
const entries = store.entries;
|
||||
expect(entries.length).toBe(1);
|
||||
expect(entries[0].kind).toBe("standalone");
|
||||
});
|
||||
|
||||
it("counts unread incoming messages in unreadCount", () => {
|
||||
const store = new MailStore();
|
||||
store.inbox = [
|
||||
makeMessage({ readAt: null, createdAt: new Date(1) }),
|
||||
makeMessage({ readAt: new Date(2), createdAt: new Date(2) }),
|
||||
makeMessage({ readAt: null, createdAt: new Date(3) }),
|
||||
];
|
||||
expect(store.unreadCount).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user