6679260d0a
Thread the Telegram bot's service language (en/ru) from the session mint response through the gateway into the FlatBuffers Session, so the UI knows which bot the player signed in through. handleTelegramAuth refreshes the account's service language onto the response before minting (it was set after the fetched copy). Empty for a non-Telegram login.
606 lines
19 KiB
Plaintext
606 lines
19 KiB
Plaintext
// FlatBuffers payloads for the client <-> gateway edge transport.
|
|
//
|
|
// Every request and response that rides inside the Connect envelope
|
|
// (gateway/proto/edge) `payload` field, and every push Event payload, is one of
|
|
// these tables. They are the binary wire contract shared with the UI, which
|
|
// generates TypeScript from this same schema. A single namespace keeps
|
|
// nested tables (GameView inside MoveResult / MatchResult) free of
|
|
// cross-namespace imports. Keep this schema and the backend JSON DTOs in lockstep
|
|
// — the gateway transcodes one to the other.
|
|
//
|
|
// Generate Go with `make fbs` (flatc, version-pinned). The committed output lives
|
|
// in fbs/scrabblefb/.
|
|
namespace scrabblefb;
|
|
|
|
// --- shared building blocks ---
|
|
|
|
// TileRecord is one tile in a decoded move record (history, move result, hint): its
|
|
// board coordinate, the concrete letter ("?" when read from a hand for a blank) and
|
|
// whether it came from a blank. Inbound tiles to place use PlayTile (alphabet indices).
|
|
table TileRecord {
|
|
row:int;
|
|
col:int;
|
|
letter:string;
|
|
blank:bool;
|
|
}
|
|
|
|
// PlayTile is one inbound tile to place, addressed by its alphabet index rather than a
|
|
// concrete letter. For a blank, letter carries the designated letter's index
|
|
// and blank is true. The board coordinate is its target square.
|
|
table PlayTile {
|
|
row:int;
|
|
col:int;
|
|
letter:ubyte;
|
|
blank:bool;
|
|
}
|
|
|
|
// AlphabetEntry is one letter of a variant's alphabet, sent for display only:
|
|
// index is the engine alphabet-index byte the wire uses for this letter, letter is the
|
|
// concrete character and value is its tile score. The client caches the table per variant.
|
|
table AlphabetEntry {
|
|
index:ubyte;
|
|
letter:string;
|
|
value:int;
|
|
}
|
|
|
|
// SeatView is one seat's public standing in a game. display_name is resolved by the
|
|
// backend from the account store (added trailing — backward-compatible).
|
|
table SeatView {
|
|
seat:int;
|
|
account_id:string;
|
|
score:int;
|
|
hints_used:int;
|
|
is_winner:bool;
|
|
display_name:string;
|
|
}
|
|
|
|
// GameView is the shared (non-private) game summary.
|
|
table GameView {
|
|
id:string;
|
|
variant:string;
|
|
dict_version:string;
|
|
status:string;
|
|
players:int;
|
|
to_move:int;
|
|
turn_timeout_secs:int;
|
|
multiple_words_per_turn:bool;
|
|
move_count:int;
|
|
end_reason:string;
|
|
seats:[SeatView];
|
|
// last_activity_unix is the lobby sort key: the current turn's start for an active
|
|
// game, the finish time for a finished one.
|
|
last_activity_unix:long;
|
|
}
|
|
|
|
// MoveRecord is one decoded move (a committed play, or a hint preview).
|
|
table MoveRecord {
|
|
player:int;
|
|
action:string;
|
|
dir:string;
|
|
main_row:int;
|
|
main_col:int;
|
|
tiles:[TileRecord];
|
|
words:[string];
|
|
count:int;
|
|
score:int;
|
|
total:int;
|
|
}
|
|
|
|
// --- auth (unauthenticated) ---
|
|
|
|
// TelegramLoginRequest carries the platform launch data; the gateway validates
|
|
// its HMAC before forwarding the extracted identity to the backend.
|
|
table TelegramLoginRequest {
|
|
init_data:string;
|
|
}
|
|
|
|
// GuestLoginRequest bootstraps an ephemeral guest session. locale is an optional
|
|
// preferred-language hint.
|
|
table GuestLoginRequest {
|
|
locale:string;
|
|
}
|
|
|
|
// EmailRequestRequest asks the backend to send a login confirm-code to email.
|
|
table EmailRequestRequest {
|
|
email:string;
|
|
}
|
|
|
|
// EmailLoginRequest logs in (or provisions) the account owning email, verifying
|
|
// the confirm-code.
|
|
table EmailLoginRequest {
|
|
email:string;
|
|
code:string;
|
|
}
|
|
|
|
// Session is the minted credential returned by every auth operation.
|
|
// supported_languages is the set of game languages (subset of {en, ru}, at least
|
|
// one) the service the user signed in through offers; the UI gates the New Game
|
|
// variant choice by it (en -> English; ru -> Russian + Эрудит). It is session-
|
|
// scoped (not persisted) and added trailing — backward-compatible.
|
|
table Session {
|
|
token:string;
|
|
user_id:string;
|
|
is_guest:bool;
|
|
display_name:string;
|
|
supported_languages:[string];
|
|
// service_language is the language tag (en/ru) of the Telegram bot the session was
|
|
// minted through; empty for a non-Telegram login. The UI uses it to build a share
|
|
// link that points at the same bot.
|
|
service_language:string;
|
|
}
|
|
|
|
// Ack is a simple success acknowledgement (e.g. an email-code request).
|
|
table Ack {
|
|
ok:bool;
|
|
}
|
|
|
|
// --- profile (authenticated) ---
|
|
|
|
// Profile is the authenticated account's own profile view. away_start/away_end are
|
|
// the "HH:MM" daily away-window bounds. notifications_in_app_only (default true)
|
|
// suppresses out-of-app platform push, leaving only the in-app live stream (both
|
|
// added trailing — backward-compatible).
|
|
table Profile {
|
|
user_id:string;
|
|
display_name:string;
|
|
preferred_language:string;
|
|
time_zone:string;
|
|
hint_balance:int;
|
|
block_chat:bool;
|
|
block_friend_requests:bool;
|
|
is_guest:bool;
|
|
away_start:string;
|
|
away_end:string;
|
|
notifications_in_app_only:bool = true;
|
|
}
|
|
|
|
// BlockStatus reports the caller's current manual block. The UI fetches it after any operation
|
|
// returns the "account_blocked" result code, then replaces every screen with the terminal blocked
|
|
// screen and stops all push/poll. permanent is false for a temporary block; until is an RFC3339
|
|
// UTC instant for a temporary block and empty for a permanent one; reason is the operator-set text
|
|
// resolved to the account's language, empty when none was cited.
|
|
table BlockStatus {
|
|
blocked:bool;
|
|
permanent:bool;
|
|
until:string;
|
|
reason:string;
|
|
}
|
|
|
|
// --- game (authenticated) ---
|
|
|
|
// SubmitPlayRequest places tiles on the player's turn; the backend infers the play's
|
|
// orientation from the tiles and the board. tiles are addressed by alphabet index.
|
|
table SubmitPlayRequest {
|
|
game_id:string;
|
|
tiles:[PlayTile];
|
|
}
|
|
|
|
// MoveResult is the outcome of a committed move: the move and the post-move game. rack and
|
|
// bag_len carry the actor's own post-move private state — their refilled rack (alphabet indices,
|
|
// a blank is the sentinel index 255) and the bag size after drawing — so the mover
|
|
// renders the next state straight from this response without a follow-up game.state (added
|
|
// trailing — backward-compatible).
|
|
table MoveResult {
|
|
move:MoveRecord;
|
|
game:GameView;
|
|
rack:[ubyte];
|
|
bag_len:int;
|
|
}
|
|
|
|
// StateRequest asks for the requesting player's view of a game. include_alphabet asks the
|
|
// backend to embed the variant's AlphabetEntry table in the reply; the client
|
|
// sets it only on a per-variant cache miss so the table is not resent on every poll.
|
|
table StateRequest {
|
|
game_id:string;
|
|
include_alphabet:bool = false;
|
|
}
|
|
|
|
// StateView is a player's view of a game: the shared summary plus their private rack, the
|
|
// bag size and their remaining hint budget. rack carries alphabet indices; a
|
|
// blank tile is the sentinel index 255. alphabet is present only when the request set
|
|
// include_alphabet (a display table the client caches per variant).
|
|
table StateView {
|
|
game:GameView;
|
|
seat:int;
|
|
rack:[ubyte];
|
|
bag_len:int;
|
|
hints_remaining:int;
|
|
alphabet:[AlphabetEntry];
|
|
}
|
|
|
|
// GameActionRequest carries just a game id (pass / resign / hint / history).
|
|
table GameActionRequest {
|
|
game_id:string;
|
|
}
|
|
|
|
// ExchangeRequest swaps the listed rack tiles back into the bag. tiles are alphabet
|
|
// indices; a blank is the sentinel index 255.
|
|
table ExchangeRequest {
|
|
game_id:string;
|
|
tiles:[ubyte];
|
|
}
|
|
|
|
// EvalRequest previews a tentative play without committing it. tiles are addressed by
|
|
// alphabet index; the backend infers the play's orientation from the tiles and the board.
|
|
table EvalRequest {
|
|
game_id:string;
|
|
tiles:[PlayTile];
|
|
}
|
|
|
|
// EvalResult is an unlimited move preview: legality, score, the words formed (main word
|
|
// first) and the orientation the backend inferred (dir is "H"/"V", empty when illegal).
|
|
table EvalResult {
|
|
legal:bool;
|
|
score:int;
|
|
words:[string];
|
|
dir:string;
|
|
}
|
|
|
|
// CheckWordRequest looks a word up in the game's pinned dictionary. word is a sequence of
|
|
// alphabet indices; the client constrains input to the variant's alphabet.
|
|
table CheckWordRequest {
|
|
game_id:string;
|
|
word:[ubyte];
|
|
}
|
|
|
|
// WordCheckResult is the dictionary lookup outcome.
|
|
table WordCheckResult {
|
|
word:string;
|
|
legal:bool;
|
|
}
|
|
|
|
// ComplaintRequest disputes a word-check result.
|
|
table ComplaintRequest {
|
|
game_id:string;
|
|
word:string;
|
|
note:string;
|
|
}
|
|
|
|
// HintResult is the top-ranked move plus the remaining hint budget.
|
|
table HintResult {
|
|
move:MoveRecord;
|
|
hints_remaining:int;
|
|
}
|
|
|
|
// DraftRequest saves the player's client-side composition for a game: a single
|
|
// JSON string of {rack_order, board_tiles} the client serializes itself, so the wire carries
|
|
// no tile array. The gateway forwards json verbatim to the backend, which owns its shape.
|
|
table DraftRequest {
|
|
game_id:string;
|
|
json:string;
|
|
}
|
|
|
|
// DraftView returns the player's stored composition as the same opaque JSON string (empty
|
|
// when none is stored). A draft get reuses GameActionRequest for its game id.
|
|
table DraftView {
|
|
json:string;
|
|
}
|
|
|
|
// History is a game's decoded move journal — the source for client board replay.
|
|
table History {
|
|
game_id:string;
|
|
moves:[MoveRecord];
|
|
}
|
|
|
|
// GameList is the caller's games (active and finished) for the lobby.
|
|
table GameList {
|
|
games:[GameView];
|
|
}
|
|
|
|
// --- lobby (authenticated) ---
|
|
|
|
// EnqueueRequest joins the auto-match pool for a variant under a per-turn word rule.
|
|
// multiple_words_per_turn true is standard Scrabble; false is the single-word rule.
|
|
table EnqueueRequest {
|
|
variant:string;
|
|
multiple_words_per_turn:bool;
|
|
}
|
|
|
|
// MatchResult reports whether the caller has been paired into a game yet.
|
|
table MatchResult {
|
|
matched:bool;
|
|
game:GameView;
|
|
}
|
|
|
|
// --- chat (authenticated) ---
|
|
|
|
// ChatPostRequest posts a per-game chat message.
|
|
table ChatPostRequest {
|
|
game_id:string;
|
|
body:string;
|
|
}
|
|
|
|
// ChatMessage is one stored chat message or nudge.
|
|
table ChatMessage {
|
|
id:string;
|
|
game_id:string;
|
|
sender_id:string;
|
|
kind:string;
|
|
body:string;
|
|
created_at_unix:long;
|
|
}
|
|
|
|
// ChatList is a game's chat history.
|
|
table ChatList {
|
|
messages:[ChatMessage];
|
|
}
|
|
|
|
// --- feedback (authenticated) ---
|
|
|
|
// FeedbackSubmitRequest submits a feedback message with an optional single
|
|
// attachment. attachment is the raw file bytes (empty for none); attachment_name
|
|
// carries the original file name (its extension keys the allow-list); channel is
|
|
// the submitting platform (telegram/ios/android/web). The response is an Ack.
|
|
table FeedbackSubmitRequest {
|
|
body:string;
|
|
attachment:[ubyte];
|
|
attachment_name:string;
|
|
channel:string;
|
|
}
|
|
|
|
// FeedbackReply is the operator's answer shown back to the player.
|
|
table FeedbackReply {
|
|
body:string;
|
|
replied_at_unix:long;
|
|
}
|
|
|
|
// FeedbackState is the player's feedback screen state. blocked_reason is "" (can
|
|
// send), "pending" or "banned"; reply is null when there is none to show. Fetching
|
|
// it (feedback.get) delivers any pending reply, clearing the badge.
|
|
table FeedbackState {
|
|
can_send:bool;
|
|
blocked_reason:string;
|
|
reply:FeedbackReply;
|
|
}
|
|
|
|
// FeedbackUnread reports whether the player has an undelivered operator reply, for
|
|
// the lobby/Info badge (feedback.unread; no side effect).
|
|
table FeedbackUnread {
|
|
reply_unread:bool;
|
|
}
|
|
|
|
// --- account, statistics, friends, blocks, invitations, history ---
|
|
|
|
// AccountRef is a referenced account with its display name resolved — the shared
|
|
// shape for friends, blocked users and invitation participants.
|
|
table AccountRef {
|
|
account_id:string;
|
|
display_name:string;
|
|
}
|
|
|
|
// UpdateProfileRequest overwrites the full editable profile (the client sends the
|
|
// complete desired profile). away_start/away_end are "HH:MM" bounds.
|
|
// notifications_in_app_only (trailing — backward-compatible) toggles out-of-app
|
|
// platform push off when set.
|
|
table UpdateProfileRequest {
|
|
display_name:string;
|
|
preferred_language:string;
|
|
time_zone:string;
|
|
away_start:string;
|
|
away_end:string;
|
|
block_chat:bool;
|
|
block_friend_requests:bool;
|
|
notifications_in_app_only:bool = true;
|
|
}
|
|
|
|
// --- account linking & merge (authenticated) ---
|
|
|
|
// LinkEmailRequest mails a confirm-code to email for a later link or merge. The
|
|
// code is always sent (no pre-send "taken" signal), so a probe cannot enumerate
|
|
// registered addresses.
|
|
table LinkEmailRequest {
|
|
email:string;
|
|
}
|
|
|
|
// LinkEmailConfirm carries the email and its confirm code, for both the confirm
|
|
// (preview) and the explicit merge step.
|
|
table LinkEmailConfirm {
|
|
email:string;
|
|
code:string;
|
|
}
|
|
|
|
// LinkTelegramRequest carries Telegram Login Widget data (a URL query string) for
|
|
// attaching a Telegram identity to the current account.
|
|
table LinkTelegramRequest {
|
|
data:string;
|
|
}
|
|
|
|
// LinkResult is the unified result of a confirm or merge step. status is "linked"
|
|
// (bound to the caller), "merge_required" (the identity belongs to another account —
|
|
// the secondary_* fields summarise it for the irreversible confirmation), or
|
|
// "merged" (done). session is present only when the active account switched (a guest
|
|
// initiator whose durable counterpart won) — the client adopts it.
|
|
table LinkResult {
|
|
status:string;
|
|
secondary_user_id:string;
|
|
secondary_display_name:string;
|
|
secondary_games:int;
|
|
secondary_friends:int;
|
|
session:Session;
|
|
}
|
|
|
|
// StatsView is a durable account's lifetime statistics (games-played and win-rate
|
|
// are derived client-side).
|
|
table StatsView {
|
|
wins:int;
|
|
losses:int;
|
|
draws:int;
|
|
max_game_points:int;
|
|
max_word_points:int;
|
|
}
|
|
|
|
// TargetRequest names a single counterpart account (friend request/cancel/unfriend,
|
|
// block/unblock).
|
|
table TargetRequest {
|
|
account_id:string;
|
|
}
|
|
|
|
// FriendRespondRequest accepts or declines a pending request from a requester.
|
|
table FriendRespondRequest {
|
|
requester_id:string;
|
|
accept:bool;
|
|
}
|
|
|
|
// FriendList is the caller's accepted friends.
|
|
table FriendList {
|
|
friends:[AccountRef];
|
|
}
|
|
|
|
// IncomingRequestList is the friend requests awaiting the caller's response.
|
|
table IncomingRequestList {
|
|
requests:[AccountRef];
|
|
}
|
|
|
|
// OutgoingRequestList is the accounts the caller has already requested and cannot
|
|
// (re-)request: a live pending request or one the addressee declined. The game's
|
|
// "add to friends" item reads it to stay disabled across reloads.
|
|
table OutgoingRequestList {
|
|
requests:[AccountRef];
|
|
}
|
|
|
|
// FriendCode is a freshly issued one-time add-a-friend code (returned once).
|
|
table FriendCode {
|
|
code:string;
|
|
expires_at_unix:long;
|
|
}
|
|
|
|
// RedeemCodeRequest redeems a friend code, befriending its issuer.
|
|
table RedeemCodeRequest {
|
|
code:string;
|
|
}
|
|
|
|
// RedeemResult reports the new friend gained by redeeming a code.
|
|
table RedeemResult {
|
|
friend:AccountRef;
|
|
}
|
|
|
|
// BlockList is the accounts the caller has blocked.
|
|
table BlockList {
|
|
blocked:[AccountRef];
|
|
}
|
|
|
|
// InvitationInvitee is one invitee's seat and response, name resolved.
|
|
table InvitationInvitee {
|
|
account_id:string;
|
|
display_name:string;
|
|
seat:int;
|
|
response:string;
|
|
}
|
|
|
|
// Invitation is a friend-game invitation with its settings and invitees.
|
|
table Invitation {
|
|
id:string;
|
|
inviter:AccountRef;
|
|
invitees:[InvitationInvitee];
|
|
variant:string;
|
|
turn_timeout_secs:int;
|
|
hints_allowed:bool;
|
|
hints_per_player:int;
|
|
multiple_words_per_turn:bool;
|
|
dropout_tiles:string;
|
|
status:string;
|
|
game_id:string;
|
|
expires_at_unix:long;
|
|
}
|
|
|
|
// CreateInvitationRequest proposes a 2-4 player friend game to the named invitees.
|
|
table CreateInvitationRequest {
|
|
invitee_ids:[string];
|
|
variant:string;
|
|
turn_timeout_secs:int;
|
|
hints_allowed:bool;
|
|
hints_per_player:int;
|
|
dropout_tiles:string;
|
|
multiple_words_per_turn:bool;
|
|
}
|
|
|
|
// InvitationActionRequest accepts / declines / cancels an invitation by id.
|
|
table InvitationActionRequest {
|
|
invitation_id:string;
|
|
}
|
|
|
|
// InvitationList is the caller's open invitations.
|
|
table InvitationList {
|
|
invitations:[Invitation];
|
|
}
|
|
|
|
// GcgExport is a finished game's GCG transcript: a suggested filename and the text.
|
|
table GcgExport {
|
|
game_id:string;
|
|
filename:string;
|
|
content:string;
|
|
}
|
|
|
|
// --- push event payloads ---
|
|
|
|
// YourTurnEvent signals that it is now the recipient's turn. The trailing fields enrich the
|
|
// out-of-app push: opponent_name is the player who just moved, last_action is their
|
|
// move kind ("play"/"pass"/"exchange"/...), last_word is the main word of a scoring play (empty
|
|
// otherwise), and score_line is the recipient-first running score (e.g. "120:95:80"). They are
|
|
// appended (FlatBuffers-optional), so an older reader that only needs game_id/deadline is unaffected.
|
|
// move_count is the post-move count (matching the opponent_moved GameView): the client uses it to
|
|
// tell whether its cached game already reflects the move, falling back to a refetch on a gap.
|
|
table YourTurnEvent {
|
|
game_id:string;
|
|
deadline_unix:long;
|
|
opponent_name:string;
|
|
last_action:string;
|
|
last_word:string;
|
|
score_line:string;
|
|
move_count:int;
|
|
}
|
|
|
|
// GameOverEvent signals that a game the recipient is seated in has finished, driving the
|
|
// out-of-app "game over" push. result is the outcome from the recipient's own
|
|
// perspective ("won"/"lost"/"draw"); score_line is the recipient-first final score. game is the
|
|
// final post-game summary (adjusted scores after rack penalties + the winner flag), so the client
|
|
// settles the finished game from the event without a refetch (added trailing).
|
|
table GameOverEvent {
|
|
game_id:string;
|
|
result:string;
|
|
score_line:string;
|
|
game:GameView;
|
|
}
|
|
|
|
// OpponentMovedEvent carries a move another seat just committed as a delta the client applies to
|
|
// its cached game without a refetch: move is the decoded play/pass/exchange (the same record
|
|
// game.history returns), game is the post-move summary (per-seat scores, to_move, move_count,
|
|
// status) and bag_len is the bag size after the draw.
|
|
table OpponentMovedEvent {
|
|
game_id:string;
|
|
move:MoveRecord;
|
|
game:GameView;
|
|
bag_len:int;
|
|
}
|
|
|
|
// NudgeEvent signals that a player nudged the recipient.
|
|
table NudgeEvent {
|
|
game_id:string;
|
|
from_user_id:string;
|
|
}
|
|
|
|
// MatchFoundEvent signals that an auto-match pairing (or robot substitution) started a game the
|
|
// recipient is seated in. state is the recipient's full initial view of the new game (empty board,
|
|
// dealt rack), so the client navigates straight in from the event with no follow-up fetch (added
|
|
// trailing — an older reader still reads just game_id).
|
|
table MatchFoundEvent {
|
|
game_id:string;
|
|
state:StateView;
|
|
}
|
|
|
|
// NotificationEvent is a lightweight "something changed, re-poll" signal that
|
|
// drives the lobby badge (incoming friend requests, invitations). kind is a sub-
|
|
// discriminator ("friend_request", "friend_added", "friend_declined", "invitation",
|
|
// "game_started"); the client re-fetches its lobby counters (and, for a requester
|
|
// watching a game, its friend state) on any of them. To let the client update its lobby without a
|
|
// follow-up fetch, each event also carries the payload its kind changed: account for the
|
|
// friend_* kinds (the requester/friend), invitation for "invitation" (the new invitation) and
|
|
// state for "game_started" (the started game's initial view, like match_found). Only the field
|
|
// matching kind is set (all added trailing — backward-compatible).
|
|
table NotificationEvent {
|
|
kind:string;
|
|
account:AccountRef;
|
|
invitation:Invitation;
|
|
state:StateView;
|
|
}
|