52f898ca6f
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 11s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
Link an email (confirm-code) or Telegram (web Login Widget) to the current account; if the identity already has its own account, merge the two into the one in use (the current account is primary, except a guest initiator whose durable counterpart wins). The merge runs in one transaction (internal/accountmerge): stats + hint wallet summed, paid_account ORed, identities/games/chat/complaints transferred, friends/blocks de-duplicated, the secondary kept as a merged_into tombstone so a shared finished game's no-cascade FKs hold; a shared active game blocks the merge. - migration 00009: accounts.paid_account, merged_into, merged_at (+ jetgen) - internal/link orchestrator; session.RevokeAllForAccount on merge - connector ValidateLoginWidget RPC + loginwidget HMAC validator - edge ops link.email.request/confirm/merge, link.telegram.confirm/merge; supersedes the Stage 8 email.bind.* surface (request never reveals 'taken' before the code is verified, so a probe cannot enumerate addresses) - UI Profile link section + irreversible-merge dialog; Telegram web sign-in - focused regression tests (merge core, guest inversion, active-game refusal, finished-shared-game kept), gateway transcode + connector + UI codec/e2e - docs: PLAN, ARCHITECTURE 3/4/9, FUNCTIONAL(+ru), module READMEs
628 lines
21 KiB
TypeScript
628 lines
21 KiB
TypeScript
// FlatBuffers <-> domain-model codec. The real transport encodes each request table
|
|
// and decodes each response/event table here, mirroring the gateway's Go encoders in
|
|
// reverse. The screens only ever see the plain model (lib/model), never these wire
|
|
// types.
|
|
|
|
import { Builder, ByteBuffer, type Offset } from 'flatbuffers';
|
|
import * as fb from '../gen/fbs/scrabblefb';
|
|
import type { PlacedTile } from './client';
|
|
import type {
|
|
AccountRef,
|
|
ChatMessage,
|
|
EvalResult,
|
|
FriendCode,
|
|
GameList,
|
|
GameView,
|
|
GcgExport,
|
|
History,
|
|
HintResult,
|
|
Invitation,
|
|
InvitationInvitee,
|
|
InvitationSettings,
|
|
LinkResult,
|
|
MatchResult,
|
|
MoveRecord,
|
|
MoveResult,
|
|
Profile,
|
|
ProfileUpdate,
|
|
PushEvent,
|
|
Seat,
|
|
Session,
|
|
StateView,
|
|
Stats,
|
|
Tile,
|
|
Variant,
|
|
WordCheckResult,
|
|
} from './model';
|
|
|
|
// --- request encoders ---
|
|
|
|
function buildTile(b: Builder, t: PlacedTile): Offset {
|
|
const letter = b.createString(t.letter);
|
|
fb.TileRecord.startTileRecord(b);
|
|
fb.TileRecord.addRow(b, t.row);
|
|
fb.TileRecord.addCol(b, t.col);
|
|
fb.TileRecord.addLetter(b, letter);
|
|
fb.TileRecord.addBlank(b, t.blank);
|
|
return fb.TileRecord.endTileRecord(b);
|
|
}
|
|
|
|
function finish(b: Builder, root: Offset): Uint8Array {
|
|
b.finish(root);
|
|
return b.asUint8Array();
|
|
}
|
|
|
|
export const empty = (): Uint8Array => new Uint8Array();
|
|
|
|
export function encodeGameAction(gameId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const gid = b.createString(gameId);
|
|
fb.GameActionRequest.startGameActionRequest(b);
|
|
fb.GameActionRequest.addGameId(b, gid);
|
|
return finish(b, fb.GameActionRequest.endGameActionRequest(b));
|
|
}
|
|
|
|
export function encodeStateRequest(gameId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const gid = b.createString(gameId);
|
|
fb.StateRequest.startStateRequest(b);
|
|
fb.StateRequest.addGameId(b, gid);
|
|
return finish(b, fb.StateRequest.endStateRequest(b));
|
|
}
|
|
|
|
export function encodeSubmitPlay(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
|
const b = new Builder(256);
|
|
const tileOffs = tiles.map((t) => buildTile(b, t));
|
|
const vec = fb.SubmitPlayRequest.createTilesVector(b, tileOffs);
|
|
const gid = b.createString(gameId);
|
|
const d = b.createString(dir);
|
|
fb.SubmitPlayRequest.startSubmitPlayRequest(b);
|
|
fb.SubmitPlayRequest.addGameId(b, gid);
|
|
fb.SubmitPlayRequest.addDir(b, d);
|
|
fb.SubmitPlayRequest.addTiles(b, vec);
|
|
return finish(b, fb.SubmitPlayRequest.endSubmitPlayRequest(b));
|
|
}
|
|
|
|
export function encodeEval(gameId: string, dir: 'H' | 'V', tiles: PlacedTile[]): Uint8Array {
|
|
const b = new Builder(256);
|
|
const tileOffs = tiles.map((t) => buildTile(b, t));
|
|
const vec = fb.EvalRequest.createTilesVector(b, tileOffs);
|
|
const gid = b.createString(gameId);
|
|
const d = b.createString(dir);
|
|
fb.EvalRequest.startEvalRequest(b);
|
|
fb.EvalRequest.addGameId(b, gid);
|
|
fb.EvalRequest.addDir(b, d);
|
|
fb.EvalRequest.addTiles(b, vec);
|
|
return finish(b, fb.EvalRequest.endEvalRequest(b));
|
|
}
|
|
|
|
export function encodeExchange(gameId: string, tiles: string[]): Uint8Array {
|
|
const b = new Builder(128);
|
|
const offs = tiles.map((s) => b.createString(s));
|
|
const vec = fb.ExchangeRequest.createTilesVector(b, offs);
|
|
const gid = b.createString(gameId);
|
|
fb.ExchangeRequest.startExchangeRequest(b);
|
|
fb.ExchangeRequest.addGameId(b, gid);
|
|
fb.ExchangeRequest.addTiles(b, vec);
|
|
return finish(b, fb.ExchangeRequest.endExchangeRequest(b));
|
|
}
|
|
|
|
export function encodeCheckWord(gameId: string, word: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const gid = b.createString(gameId);
|
|
const w = b.createString(word);
|
|
fb.CheckWordRequest.startCheckWordRequest(b);
|
|
fb.CheckWordRequest.addGameId(b, gid);
|
|
fb.CheckWordRequest.addWord(b, w);
|
|
return finish(b, fb.CheckWordRequest.endCheckWordRequest(b));
|
|
}
|
|
|
|
export function encodeComplaint(gameId: string, word: string, note: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const gid = b.createString(gameId);
|
|
const w = b.createString(word);
|
|
const n = b.createString(note);
|
|
fb.ComplaintRequest.startComplaintRequest(b);
|
|
fb.ComplaintRequest.addGameId(b, gid);
|
|
fb.ComplaintRequest.addWord(b, w);
|
|
fb.ComplaintRequest.addNote(b, n);
|
|
return finish(b, fb.ComplaintRequest.endComplaintRequest(b));
|
|
}
|
|
|
|
export function encodeEnqueue(variant: Variant): Uint8Array {
|
|
const b = new Builder(64);
|
|
const v = b.createString(variant);
|
|
fb.EnqueueRequest.startEnqueueRequest(b);
|
|
fb.EnqueueRequest.addVariant(b, v);
|
|
return finish(b, fb.EnqueueRequest.endEnqueueRequest(b));
|
|
}
|
|
|
|
export function encodeChatPost(gameId: string, body: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const gid = b.createString(gameId);
|
|
const bd = b.createString(body);
|
|
fb.ChatPostRequest.startChatPostRequest(b);
|
|
fb.ChatPostRequest.addGameId(b, gid);
|
|
fb.ChatPostRequest.addBody(b, bd);
|
|
return finish(b, fb.ChatPostRequest.endChatPostRequest(b));
|
|
}
|
|
|
|
export function encodeTelegramLogin(initData: string): Uint8Array {
|
|
const b = new Builder(512);
|
|
const d = b.createString(initData);
|
|
fb.TelegramLoginRequest.startTelegramLoginRequest(b);
|
|
fb.TelegramLoginRequest.addInitData(b, d);
|
|
return finish(b, fb.TelegramLoginRequest.endTelegramLoginRequest(b));
|
|
}
|
|
|
|
export function encodeGuestLogin(locale: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const l = b.createString(locale);
|
|
fb.GuestLoginRequest.startGuestLoginRequest(b);
|
|
fb.GuestLoginRequest.addLocale(b, l);
|
|
return finish(b, fb.GuestLoginRequest.endGuestLoginRequest(b));
|
|
}
|
|
|
|
export function encodeEmailRequest(email: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
fb.EmailRequestRequest.startEmailRequestRequest(b);
|
|
fb.EmailRequestRequest.addEmail(b, e);
|
|
return finish(b, fb.EmailRequestRequest.endEmailRequestRequest(b));
|
|
}
|
|
|
|
export function encodeEmailLogin(email: string, code: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
const c = b.createString(code);
|
|
fb.EmailLoginRequest.startEmailLoginRequest(b);
|
|
fb.EmailLoginRequest.addEmail(b, e);
|
|
fb.EmailLoginRequest.addCode(b, c);
|
|
return finish(b, fb.EmailLoginRequest.endEmailLoginRequest(b));
|
|
}
|
|
|
|
// --- response decoders ---
|
|
|
|
function s(v: string | null): string {
|
|
return v ?? '';
|
|
}
|
|
|
|
function decodeTile(t: fb.TileRecord): Tile {
|
|
return { row: t.row(), col: t.col(), letter: s(t.letter()), blank: t.blank() };
|
|
}
|
|
|
|
function decodeSeat(v: fb.SeatView): Seat {
|
|
return {
|
|
seat: v.seat(),
|
|
accountId: s(v.accountId()),
|
|
displayName: s(v.displayName()),
|
|
score: v.score(),
|
|
hintsUsed: v.hintsUsed(),
|
|
isWinner: v.isWinner(),
|
|
};
|
|
}
|
|
|
|
function decodeGameView(g: fb.GameView): GameView {
|
|
const seats: Seat[] = [];
|
|
for (let i = 0; i < g.seatsLength(); i++) {
|
|
const sv = g.seats(i);
|
|
if (sv) seats.push(decodeSeat(sv));
|
|
}
|
|
return {
|
|
id: s(g.id()),
|
|
variant: s(g.variant()) as Variant,
|
|
dictVersion: s(g.dictVersion()),
|
|
status: s(g.status()),
|
|
players: g.players(),
|
|
toMove: g.toMove(),
|
|
turnTimeoutSecs: g.turnTimeoutSecs(),
|
|
moveCount: g.moveCount(),
|
|
endReason: s(g.endReason()),
|
|
seats,
|
|
};
|
|
}
|
|
|
|
function decodeMove(m: fb.MoveRecord): MoveRecord {
|
|
const tiles: Tile[] = [];
|
|
for (let i = 0; i < m.tilesLength(); i++) {
|
|
const t = m.tiles(i);
|
|
if (t) tiles.push(decodeTile(t));
|
|
}
|
|
const words: string[] = [];
|
|
for (let i = 0; i < m.wordsLength(); i++) words.push(s(m.words(i)));
|
|
return {
|
|
player: m.player(),
|
|
action: s(m.action()),
|
|
dir: s(m.dir()),
|
|
mainRow: m.mainRow(),
|
|
mainCol: m.mainCol(),
|
|
tiles,
|
|
words,
|
|
count: m.count(),
|
|
score: m.score(),
|
|
total: m.total(),
|
|
};
|
|
}
|
|
|
|
function decodeChatMsg(m: fb.ChatMessage): ChatMessage {
|
|
return {
|
|
id: s(m.id()),
|
|
gameId: s(m.gameId()),
|
|
senderId: s(m.senderId()),
|
|
kind: s(m.kind()),
|
|
body: s(m.body()),
|
|
createdAtUnix: Number(m.createdAtUnix()),
|
|
};
|
|
}
|
|
|
|
export function decodeSession(buf: Uint8Array): Session {
|
|
const t = fb.Session.getRootAsSession(new ByteBuffer(buf));
|
|
return { token: s(t.token()), userId: s(t.userId()), isGuest: t.isGuest(), displayName: s(t.displayName()) };
|
|
}
|
|
|
|
export function decodeProfile(buf: Uint8Array): Profile {
|
|
const p = fb.Profile.getRootAsProfile(new ByteBuffer(buf));
|
|
return {
|
|
userId: s(p.userId()),
|
|
displayName: s(p.displayName()),
|
|
preferredLanguage: s(p.preferredLanguage()),
|
|
timeZone: s(p.timeZone()),
|
|
awayStart: s(p.awayStart()),
|
|
awayEnd: s(p.awayEnd()),
|
|
hintBalance: p.hintBalance(),
|
|
blockChat: p.blockChat(),
|
|
blockFriendRequests: p.blockFriendRequests(),
|
|
isGuest: p.isGuest(),
|
|
notificationsInAppOnly: p.notificationsInAppOnly(),
|
|
};
|
|
}
|
|
|
|
export function decodeStateView(buf: Uint8Array): StateView {
|
|
const v = fb.StateView.getRootAsStateView(new ByteBuffer(buf));
|
|
const g = v.game();
|
|
const rack: string[] = [];
|
|
for (let i = 0; i < v.rackLength(); i++) rack.push(s(v.rack(i)));
|
|
return {
|
|
game: g ? decodeGameView(g) : emptyGame(),
|
|
seat: v.seat(),
|
|
rack,
|
|
bagLen: v.bagLen(),
|
|
hintsRemaining: v.hintsRemaining(),
|
|
};
|
|
}
|
|
|
|
export function decodeMoveResult(buf: Uint8Array): MoveResult {
|
|
const r = fb.MoveResult.getRootAsMoveResult(new ByteBuffer(buf));
|
|
const m = r.move();
|
|
const g = r.game();
|
|
return { move: m ? decodeMove(m) : emptyMove(), game: g ? decodeGameView(g) : emptyGame() };
|
|
}
|
|
|
|
export function decodeHintResult(buf: Uint8Array): HintResult {
|
|
const r = fb.HintResult.getRootAsHintResult(new ByteBuffer(buf));
|
|
const m = r.move();
|
|
return { move: m ? decodeMove(m) : emptyMove(), hintsRemaining: r.hintsRemaining() };
|
|
}
|
|
|
|
export function decodeEvalResult(buf: Uint8Array): EvalResult {
|
|
const r = fb.EvalResult.getRootAsEvalResult(new ByteBuffer(buf));
|
|
const words: string[] = [];
|
|
for (let i = 0; i < r.wordsLength(); i++) words.push(s(r.words(i)));
|
|
return { legal: r.legal(), score: r.score(), words };
|
|
}
|
|
|
|
export function decodeWordCheck(buf: Uint8Array): WordCheckResult {
|
|
const r = fb.WordCheckResult.getRootAsWordCheckResult(new ByteBuffer(buf));
|
|
return { word: s(r.word()), legal: r.legal() };
|
|
}
|
|
|
|
export function decodeHistory(buf: Uint8Array): History {
|
|
const h = fb.History.getRootAsHistory(new ByteBuffer(buf));
|
|
const moves: MoveRecord[] = [];
|
|
for (let i = 0; i < h.movesLength(); i++) {
|
|
const m = h.moves(i);
|
|
if (m) moves.push(decodeMove(m));
|
|
}
|
|
return { gameId: s(h.gameId()), moves };
|
|
}
|
|
|
|
export function decodeGameList(buf: Uint8Array): GameList {
|
|
const gl = fb.GameList.getRootAsGameList(new ByteBuffer(buf));
|
|
const games: GameView[] = [];
|
|
for (let i = 0; i < gl.gamesLength(); i++) {
|
|
const g = gl.games(i);
|
|
if (g) games.push(decodeGameView(g));
|
|
}
|
|
return { games };
|
|
}
|
|
|
|
export function decodeMatchResult(buf: Uint8Array): MatchResult {
|
|
const m = fb.MatchResult.getRootAsMatchResult(new ByteBuffer(buf));
|
|
const g = m.game();
|
|
return { matched: m.matched(), game: m.matched() && g ? decodeGameView(g) : undefined };
|
|
}
|
|
|
|
export function decodeChatMessage(buf: Uint8Array): ChatMessage {
|
|
return decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(new ByteBuffer(buf)));
|
|
}
|
|
|
|
export function decodeChatList(buf: Uint8Array): ChatMessage[] {
|
|
const cl = fb.ChatList.getRootAsChatList(new ByteBuffer(buf));
|
|
const out: ChatMessage[] = [];
|
|
for (let i = 0; i < cl.messagesLength(); i++) {
|
|
const m = cl.messages(i);
|
|
if (m) out.push(decodeChatMsg(m));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeEvent(kind: string, payload: Uint8Array): PushEvent | null {
|
|
const bb = new ByteBuffer(payload);
|
|
switch (kind) {
|
|
case 'your_turn': {
|
|
const e = fb.YourTurnEvent.getRootAsYourTurnEvent(bb);
|
|
return { kind: 'your_turn', gameId: s(e.gameId()), deadlineUnix: Number(e.deadlineUnix()) };
|
|
}
|
|
case 'opponent_moved': {
|
|
const e = fb.OpponentMovedEvent.getRootAsOpponentMovedEvent(bb);
|
|
return { kind: 'opponent_moved', gameId: s(e.gameId()), seat: e.seat(), action: s(e.action()), score: e.score(), total: e.total() };
|
|
}
|
|
case 'chat_message':
|
|
return { kind: 'chat_message', message: decodeChatMsg(fb.ChatMessage.getRootAsChatMessage(bb)) };
|
|
case 'nudge': {
|
|
const e = fb.NudgeEvent.getRootAsNudgeEvent(bb);
|
|
return { kind: 'nudge', gameId: s(e.gameId()), fromUserId: s(e.fromUserId()) };
|
|
}
|
|
case 'match_found': {
|
|
const e = fb.MatchFoundEvent.getRootAsMatchFoundEvent(bb);
|
|
return { kind: 'match_found', gameId: s(e.gameId()) };
|
|
}
|
|
case 'notify': {
|
|
const e = fb.NotificationEvent.getRootAsNotificationEvent(bb);
|
|
return { kind: 'notify', sub: s(e.kind()) };
|
|
}
|
|
case 'heartbeat':
|
|
return { kind: 'heartbeat' };
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// --- Stage 8 encoders ---
|
|
|
|
export function encodeTarget(accountId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(accountId);
|
|
fb.TargetRequest.startTargetRequest(b);
|
|
fb.TargetRequest.addAccountId(b, id);
|
|
return finish(b, fb.TargetRequest.endTargetRequest(b));
|
|
}
|
|
|
|
export function encodeFriendRespond(requesterId: string, accept: boolean): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(requesterId);
|
|
fb.FriendRespondRequest.startFriendRespondRequest(b);
|
|
fb.FriendRespondRequest.addRequesterId(b, id);
|
|
fb.FriendRespondRequest.addAccept(b, accept);
|
|
return finish(b, fb.FriendRespondRequest.endFriendRespondRequest(b));
|
|
}
|
|
|
|
export function encodeRedeemCode(code: string): Uint8Array {
|
|
const b = new Builder(32);
|
|
const c = b.createString(code);
|
|
fb.RedeemCodeRequest.startRedeemCodeRequest(b);
|
|
fb.RedeemCodeRequest.addCode(b, c);
|
|
return finish(b, fb.RedeemCodeRequest.endRedeemCodeRequest(b));
|
|
}
|
|
|
|
export function encodeCreateInvitation(inviteeIds: string[], st: InvitationSettings): Uint8Array {
|
|
const b = new Builder(256);
|
|
const idOffs = inviteeIds.map((id) => b.createString(id));
|
|
const ids = fb.CreateInvitationRequest.createInviteeIdsVector(b, idOffs);
|
|
const variant = b.createString(st.variant);
|
|
const dropout = b.createString(st.dropoutTiles);
|
|
fb.CreateInvitationRequest.startCreateInvitationRequest(b);
|
|
fb.CreateInvitationRequest.addInviteeIds(b, ids);
|
|
fb.CreateInvitationRequest.addVariant(b, variant);
|
|
fb.CreateInvitationRequest.addTurnTimeoutSecs(b, st.turnTimeoutSecs);
|
|
fb.CreateInvitationRequest.addHintsAllowed(b, st.hintsAllowed);
|
|
fb.CreateInvitationRequest.addHintsPerPlayer(b, st.hintsPerPlayer);
|
|
fb.CreateInvitationRequest.addDropoutTiles(b, dropout);
|
|
return finish(b, fb.CreateInvitationRequest.endCreateInvitationRequest(b));
|
|
}
|
|
|
|
export function encodeInvitationAction(invitationId: string): Uint8Array {
|
|
const b = new Builder(64);
|
|
const id = b.createString(invitationId);
|
|
fb.InvitationActionRequest.startInvitationActionRequest(b);
|
|
fb.InvitationActionRequest.addInvitationId(b, id);
|
|
return finish(b, fb.InvitationActionRequest.endInvitationActionRequest(b));
|
|
}
|
|
|
|
export function encodeUpdateProfile(p: ProfileUpdate): Uint8Array {
|
|
const b = new Builder(256);
|
|
const name = b.createString(p.displayName);
|
|
const lang = b.createString(p.preferredLanguage);
|
|
const tz = b.createString(p.timeZone);
|
|
const as = b.createString(p.awayStart);
|
|
const ae = b.createString(p.awayEnd);
|
|
fb.UpdateProfileRequest.startUpdateProfileRequest(b);
|
|
fb.UpdateProfileRequest.addDisplayName(b, name);
|
|
fb.UpdateProfileRequest.addPreferredLanguage(b, lang);
|
|
fb.UpdateProfileRequest.addTimeZone(b, tz);
|
|
fb.UpdateProfileRequest.addAwayStart(b, as);
|
|
fb.UpdateProfileRequest.addAwayEnd(b, ae);
|
|
fb.UpdateProfileRequest.addBlockChat(b, p.blockChat);
|
|
fb.UpdateProfileRequest.addBlockFriendRequests(b, p.blockFriendRequests);
|
|
fb.UpdateProfileRequest.addNotificationsInAppOnly(b, p.notificationsInAppOnly);
|
|
return finish(b, fb.UpdateProfileRequest.endUpdateProfileRequest(b));
|
|
}
|
|
|
|
// --- account linking & merge (Stage 11) ---
|
|
|
|
export function encodeLinkEmailRequest(email: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
fb.LinkEmailRequest.startLinkEmailRequest(b);
|
|
fb.LinkEmailRequest.addEmail(b, e);
|
|
return finish(b, fb.LinkEmailRequest.endLinkEmailRequest(b));
|
|
}
|
|
|
|
export function encodeLinkEmailConfirm(email: string, code: string): Uint8Array {
|
|
const b = new Builder(128);
|
|
const e = b.createString(email);
|
|
const c = b.createString(code);
|
|
fb.LinkEmailConfirm.startLinkEmailConfirm(b);
|
|
fb.LinkEmailConfirm.addEmail(b, e);
|
|
fb.LinkEmailConfirm.addCode(b, c);
|
|
return finish(b, fb.LinkEmailConfirm.endLinkEmailConfirm(b));
|
|
}
|
|
|
|
export function encodeLinkTelegram(data: string): Uint8Array {
|
|
const b = new Builder(256);
|
|
const d = b.createString(data);
|
|
fb.LinkTelegramRequest.startLinkTelegramRequest(b);
|
|
fb.LinkTelegramRequest.addData(b, d);
|
|
return finish(b, fb.LinkTelegramRequest.endLinkTelegramRequest(b));
|
|
}
|
|
|
|
export function decodeLinkResult(buf: Uint8Array): LinkResult {
|
|
const r = fb.LinkResult.getRootAsLinkResult(new ByteBuffer(buf));
|
|
const sess = r.session();
|
|
return {
|
|
status: (s(r.status()) || 'linked') as LinkResult['status'],
|
|
secondaryUserId: s(r.secondaryUserId()),
|
|
secondaryDisplayName: s(r.secondaryDisplayName()),
|
|
secondaryGames: r.secondaryGames(),
|
|
secondaryFriends: r.secondaryFriends(),
|
|
session: sess
|
|
? { token: s(sess.token()), userId: s(sess.userId()), isGuest: sess.isGuest(), displayName: s(sess.displayName()) }
|
|
: null,
|
|
};
|
|
}
|
|
|
|
// --- Stage 8 decoders ---
|
|
|
|
function decodeAccountRef(r: fb.AccountRef): AccountRef {
|
|
return { accountId: s(r.accountId()), displayName: s(r.displayName()) };
|
|
}
|
|
|
|
export function decodeFriendList(buf: Uint8Array): AccountRef[] {
|
|
const l = fb.FriendList.getRootAsFriendList(new ByteBuffer(buf));
|
|
const out: AccountRef[] = [];
|
|
for (let i = 0; i < l.friendsLength(); i++) {
|
|
const r = l.friends(i);
|
|
if (r) out.push(decodeAccountRef(r));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeIncomingList(buf: Uint8Array): AccountRef[] {
|
|
const l = fb.IncomingRequestList.getRootAsIncomingRequestList(new ByteBuffer(buf));
|
|
const out: AccountRef[] = [];
|
|
for (let i = 0; i < l.requestsLength(); i++) {
|
|
const r = l.requests(i);
|
|
if (r) out.push(decodeAccountRef(r));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeBlockList(buf: Uint8Array): AccountRef[] {
|
|
const l = fb.BlockList.getRootAsBlockList(new ByteBuffer(buf));
|
|
const out: AccountRef[] = [];
|
|
for (let i = 0; i < l.blockedLength(); i++) {
|
|
const r = l.blocked(i);
|
|
if (r) out.push(decodeAccountRef(r));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeFriendCode(buf: Uint8Array): FriendCode {
|
|
const c = fb.FriendCode.getRootAsFriendCode(new ByteBuffer(buf));
|
|
return { code: s(c.code()), expiresAtUnix: Number(c.expiresAtUnix()) };
|
|
}
|
|
|
|
export function decodeRedeemResult(buf: Uint8Array): AccountRef {
|
|
const r = fb.RedeemResult.getRootAsRedeemResult(new ByteBuffer(buf));
|
|
const f = r.friend();
|
|
return f ? decodeAccountRef(f) : { accountId: '', displayName: '' };
|
|
}
|
|
|
|
export function decodeStats(buf: Uint8Array): Stats {
|
|
const v = fb.StatsView.getRootAsStatsView(new ByteBuffer(buf));
|
|
return {
|
|
wins: v.wins(),
|
|
losses: v.losses(),
|
|
draws: v.draws(),
|
|
maxGamePoints: v.maxGamePoints(),
|
|
maxWordPoints: v.maxWordPoints(),
|
|
};
|
|
}
|
|
|
|
function decodeInvitationTable(i: fb.Invitation): Invitation {
|
|
const inviter = i.inviter();
|
|
const invitees: InvitationInvitee[] = [];
|
|
for (let k = 0; k < i.inviteesLength(); k++) {
|
|
const iv = i.invitees(k);
|
|
if (iv) {
|
|
invitees.push({
|
|
accountId: s(iv.accountId()),
|
|
displayName: s(iv.displayName()),
|
|
seat: iv.seat(),
|
|
response: s(iv.response()),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
id: s(i.id()),
|
|
inviter: inviter ? decodeAccountRef(inviter) : { accountId: '', displayName: '' },
|
|
invitees,
|
|
variant: s(i.variant()) as Variant,
|
|
turnTimeoutSecs: i.turnTimeoutSecs(),
|
|
hintsAllowed: i.hintsAllowed(),
|
|
hintsPerPlayer: i.hintsPerPlayer(),
|
|
dropoutTiles: s(i.dropoutTiles()),
|
|
status: s(i.status()),
|
|
gameId: s(i.gameId()),
|
|
expiresAtUnix: Number(i.expiresAtUnix()),
|
|
};
|
|
}
|
|
|
|
export function decodeInvitation(buf: Uint8Array): Invitation {
|
|
return decodeInvitationTable(fb.Invitation.getRootAsInvitation(new ByteBuffer(buf)));
|
|
}
|
|
|
|
export function decodeInvitationList(buf: Uint8Array): Invitation[] {
|
|
const l = fb.InvitationList.getRootAsInvitationList(new ByteBuffer(buf));
|
|
const out: Invitation[] = [];
|
|
for (let i = 0; i < l.invitationsLength(); i++) {
|
|
const inv = l.invitations(i);
|
|
if (inv) out.push(decodeInvitationTable(inv));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function decodeGcg(buf: Uint8Array): GcgExport {
|
|
const g = fb.GcgExport.getRootAsGcgExport(new ByteBuffer(buf));
|
|
return { gameId: s(g.gameId()), filename: s(g.filename()), content: s(g.content()) };
|
|
}
|
|
|
|
function emptyGame(): GameView {
|
|
return {
|
|
id: '',
|
|
variant: 'english',
|
|
dictVersion: '',
|
|
status: '',
|
|
players: 0,
|
|
toMove: 0,
|
|
turnTimeoutSecs: 0,
|
|
moveCount: 0,
|
|
endReason: '',
|
|
seats: [],
|
|
};
|
|
}
|
|
|
|
function emptyMove(): MoveRecord {
|
|
return { player: 0, action: '', dir: '', mainRow: 0, mainCol: 0, tiles: [], words: [], count: 0, score: 0, total: 0 };
|
|
}
|