cf66ed7e26
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
New platform/telegram connector (own container, bot token only there): - go-telegram/bot long-poll loop: /start deep-links + Mini App launch button. - gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify (renders a localized message + deep-link button), SendToUser/SendToGameChannel (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id). - Bot API base override for Telegram's test environment; Dockerfile + compose (VPN sidecar, no public ingress); README. Gateway: - initData validation relocated from the gateway into the connector; the gateway calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token, and deletes internal/auth. - Out-of-app push: runPushPump routes events whose recipient has no live in-app stream to connector.Notify, gated by /internal/push-target + the in-app-only flag (race-free de-dup); HasSubscribers added to the push hub. Backend: - Migration 00007 accounts.notifications_in_app_only (default true) + jetgen. - ProvisionTelegram seeds a new account's language/display name from the launch fields; IdentityExternalID reverse lookup; /internal/push-target handler. UI: - Telegram Mini App launch: detect initData, apply themeParams, authTelegram, route the deep-link start_param (g/i/f); /telegram/ guard redirects outside Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle; share-to-Telegram link for a friend code. Vitest + Playwright coverage. Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only (Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN, ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
235 lines
5.3 KiB
TypeScript
235 lines
5.3 KiB
TypeScript
// Seed data for the mock transport. Enough to exercise the playable slice locally
|
||
// (pnpm start) with no backend: a profile, one active mid-game whose board already
|
||
// has tiles, and two finished games. Coordinates are 0-indexed (centre 7,7). Words do
|
||
// not need to be strictly legal here — this is a visual/interaction fixture; real
|
||
// legality and scoring come from the backend.
|
||
|
||
import type {
|
||
AccountRef,
|
||
ChatMessage,
|
||
GameView,
|
||
Invitation,
|
||
MoveRecord,
|
||
Profile,
|
||
Seat,
|
||
Session,
|
||
Stats,
|
||
} from '../model';
|
||
|
||
export const ME = 'me';
|
||
|
||
export const SESSION: Session = {
|
||
token: 'mock-token',
|
||
userId: ME,
|
||
isGuest: true,
|
||
displayName: 'You',
|
||
};
|
||
|
||
export const PROFILE: Profile = {
|
||
userId: ME,
|
||
displayName: 'You',
|
||
preferredLanguage: 'en',
|
||
timeZone: 'UTC',
|
||
awayStart: '00:00',
|
||
awayEnd: '07:00',
|
||
hintBalance: 3,
|
||
blockChat: false,
|
||
blockFriendRequests: false,
|
||
isGuest: false,
|
||
notificationsInAppOnly: true,
|
||
};
|
||
|
||
// Seed social/account data for the mock (pnpm start + Playwright). The mock profile
|
||
// is a durable account so the Stage 8 surfaces (friends, stats, history) are reachable.
|
||
export const MOCK_FRIENDS: AccountRef[] = [
|
||
{ accountId: 'ann', displayName: 'Ann' },
|
||
{ accountId: 'kaya', displayName: 'Kaya' },
|
||
];
|
||
|
||
export const MOCK_INCOMING: AccountRef[] = [{ accountId: 'rick', displayName: 'Rick' }];
|
||
|
||
export const MOCK_STATS: Stats = { wins: 7, losses: 4, draws: 1, maxGamePoints: 421, maxWordPoints: 95 };
|
||
|
||
export function mockInvitations(): Invitation[] {
|
||
return [
|
||
{
|
||
id: 'inv1',
|
||
inviter: { accountId: 'kaya', displayName: 'Kaya' },
|
||
invitees: [{ accountId: ME, displayName: 'You', seat: 1, response: 'pending' }],
|
||
variant: 'english',
|
||
turnTimeoutSecs: 86400,
|
||
hintsAllowed: true,
|
||
hintsPerPlayer: 1,
|
||
dropoutTiles: 'remove',
|
||
status: 'pending',
|
||
gameId: '',
|
||
expiresAtUnix: Math.floor(Date.now() / 1000) + 7 * 86400,
|
||
},
|
||
];
|
||
}
|
||
|
||
function seat(s: number, accountId: string, displayName: string, score: number, isWinner = false): Seat {
|
||
return { seat: s, accountId, displayName, score, hintsUsed: 0, isWinner };
|
||
}
|
||
|
||
function play(
|
||
player: number,
|
||
dir: 'H' | 'V',
|
||
tiles: Array<[number, number, string]>,
|
||
words: string[],
|
||
score: number,
|
||
total: number,
|
||
): MoveRecord {
|
||
const ts = tiles.map(([row, col, letter]) => ({ row, col, letter, blank: false }));
|
||
return {
|
||
player,
|
||
action: 'play',
|
||
dir,
|
||
mainRow: ts[0]?.row ?? 7,
|
||
mainCol: ts[0]?.col ?? 7,
|
||
tiles: ts,
|
||
words,
|
||
count: words.length,
|
||
score,
|
||
total,
|
||
};
|
||
}
|
||
|
||
export interface MockGame {
|
||
view: GameView;
|
||
moves: MoveRecord[];
|
||
rack: string[];
|
||
bagLen: number;
|
||
hintsRemaining: number;
|
||
chat: ChatMessage[];
|
||
}
|
||
|
||
// --- active game G1: english, You (seat 0) vs Ann (seat 1), your turn ---
|
||
|
||
const G1_MOVES: MoveRecord[] = [
|
||
play(0, 'H', [
|
||
[7, 5, 'H'],
|
||
[7, 6, 'E'],
|
||
[7, 7, 'L'],
|
||
[7, 8, 'L'],
|
||
[7, 9, 'O'],
|
||
], ['HELLO'], 16, 16),
|
||
play(1, 'V', [
|
||
[6, 9, 'W'],
|
||
[8, 9, 'R'],
|
||
[9, 9, 'L'],
|
||
[10, 9, 'D'],
|
||
], ['WORLD'], 9, 9),
|
||
play(0, 'H', [
|
||
[8, 10, 'A'],
|
||
[8, 11, 'T'],
|
||
], ['RAT'], 3, 19),
|
||
play(1, 'V', [
|
||
[9, 10, 'N'],
|
||
[10, 10, 'D'],
|
||
], ['AND'], 4, 13),
|
||
];
|
||
|
||
function activeGame(): MockGame {
|
||
return {
|
||
view: {
|
||
id: 'g1',
|
||
variant: 'english',
|
||
dictVersion: 'v1',
|
||
status: 'active',
|
||
players: 2,
|
||
toMove: 0,
|
||
turnTimeoutSecs: 86400,
|
||
moveCount: G1_MOVES.length,
|
||
endReason: '',
|
||
seats: [seat(0, ME, 'You', 19), seat(1, 'ann', 'Ann', 13)],
|
||
},
|
||
moves: G1_MOVES,
|
||
rack: ['R', 'E', 'T', 'I', 'N', 'A', '?'],
|
||
bagLen: 58,
|
||
hintsRemaining: 1,
|
||
chat: [
|
||
{
|
||
id: 'c1',
|
||
gameId: 'g1',
|
||
senderId: 'ann',
|
||
kind: 'message',
|
||
body: 'good luck!',
|
||
createdAtUnix: Math.floor(Date.now() / 1000) - 3600,
|
||
},
|
||
],
|
||
};
|
||
}
|
||
|
||
// --- finished games ---
|
||
|
||
function finishedG2(): MockGame {
|
||
return {
|
||
view: {
|
||
id: 'g2',
|
||
variant: 'english',
|
||
dictVersion: 'v1',
|
||
status: 'finished',
|
||
players: 2,
|
||
toMove: 0,
|
||
turnTimeoutSecs: 86400,
|
||
moveCount: 2,
|
||
endReason: 'normal',
|
||
seats: [seat(0, ME, 'You', 320, true), seat(1, 'kaya', 'Kaya', 281)],
|
||
},
|
||
moves: [
|
||
play(0, 'H', [
|
||
[7, 6, 'Q'],
|
||
[7, 7, 'U'],
|
||
[7, 8, 'I'],
|
||
[7, 9, 'Z'],
|
||
], ['QUIZ'], 48, 48),
|
||
play(1, 'V', [
|
||
[6, 9, 'J'],
|
||
[8, 9, 'A'],
|
||
[9, 9, 'M'],
|
||
], ['JAZM'], 30, 30),
|
||
],
|
||
rack: [],
|
||
bagLen: 0,
|
||
hintsRemaining: 0,
|
||
chat: [],
|
||
};
|
||
}
|
||
|
||
function finishedG3(): MockGame {
|
||
return {
|
||
view: {
|
||
id: 'g3',
|
||
variant: 'russian',
|
||
dictVersion: 'v1',
|
||
status: 'finished',
|
||
players: 2,
|
||
toMove: 0,
|
||
turnTimeoutSecs: 86400,
|
||
moveCount: 1,
|
||
endReason: 'resignation',
|
||
seats: [seat(0, ME, 'You', 150), seat(1, 'rick', 'Rick', 212, true)],
|
||
},
|
||
moves: [
|
||
play(0, 'H', [
|
||
[7, 6, 'С'],
|
||
[7, 7, 'Л'],
|
||
[7, 8, 'О'],
|
||
[7, 9, 'В'],
|
||
[7, 10, 'О'],
|
||
], ['СЛОВО'], 12, 12),
|
||
],
|
||
rack: [],
|
||
bagLen: 0,
|
||
hintsRemaining: 0,
|
||
chat: [],
|
||
};
|
||
}
|
||
|
||
export function seedGames(): Map<string, MockGame> {
|
||
const m = new Map<string, MockGame>();
|
||
for (const g of [activeGame(), finishedG2(), finishedG3()]) m.set(g.view.id, g);
|
||
return m;
|
||
}
|