Stage 9: Telegram integration (connector side-service, Mini App, out-of-app push)
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
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.
This commit is contained in:
+42
-18
@@ -43,9 +43,15 @@ Three executables plus per-platform side-services:
|
||||
a server-driven channel later, §10), and a client **board-style** setting (bonus-label
|
||||
mode). The visual/interaction design system is documented in
|
||||
[`UI_DESIGN.md`](UI_DESIGN.md).
|
||||
- **`platform/<name>`** *(planned)* — per-platform side-services (Telegram bot
|
||||
first): deep-link invites and platform-native push notifications. They talk
|
||||
to `backend` over an internal API.
|
||||
- **`platform/telegram`** — the Telegram side-service (the "connector", module
|
||||
`scrabble/platform/telegram`). It is the only component holding the bot token: it
|
||||
runs the Bot API long-poll loop (Mini App launch + `/start` deep-links) and serves
|
||||
a gRPC API (`pkg/proto/telegram/v1`) that `gateway` (Mini App initData validation
|
||||
and out-of-app push) and `backend` (admin messaging — Stage 10) call over the
|
||||
trusted internal network. Its generic delivery methods are **platform-agnostic**
|
||||
(keyed by the identity `external_id`), so a future VK/MAX connector reuses them; only
|
||||
initData validation is Telegram-specific. It runs in its own container, egressing to
|
||||
Telegram through a VPN sidecar.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -55,7 +61,9 @@ flowchart LR
|
||||
Gateway -- in-app stream --> Client
|
||||
Backend -- pgx --> Postgres[(Postgres)]
|
||||
Backend -. embeds .- Solver[[scrabble-solver library]]
|
||||
Telegram[Telegram bot side-service] -- internal API --> Backend
|
||||
Gateway -- gRPC (validate initData, out-of-app push) --> Telegram[Telegram connector]
|
||||
Backend -. admin gRPC, Stage 10 .-> Telegram
|
||||
Telegram -- Bot API (via VPN sidecar) --> TgCloud((Telegram))
|
||||
```
|
||||
|
||||
The MVP runs `gateway` and `backend` as single-instance processes inside a
|
||||
@@ -92,10 +100,12 @@ Platform-native, deliberately simple: **no Ed25519 client keys, no per-request
|
||||
signing, no anti-replay crypto** (these were considered and dropped — players
|
||||
arrive from a platform rather than completing a mandatory registration).
|
||||
|
||||
- The gateway validates the originating credential **once** — the platform's
|
||||
signed launch data (e.g. Telegram `initData` HMAC), an email-code login, or a
|
||||
guest bootstrap — then mints a **thin opaque server session token**
|
||||
(`session_id`).
|
||||
- The gateway validates the originating credential **once** — Telegram `initData`
|
||||
(delegated to the connector's `ValidateInitData` RPC, which holds the bot token —
|
||||
the HMAC secret — so it never reaches the gateway), an email-code login, or a guest
|
||||
bootstrap — then mints a **thin opaque server session token** (`session_id`). First
|
||||
Telegram contact seeds the new account's language (from the launch `language_code`)
|
||||
and display name (§4).
|
||||
- The client holds `session_id` in memory for the app session (browser/OS
|
||||
storage is optional and may be unavailable; losing it means re-login).
|
||||
- The gateway caches `session → user_id` and injects `X-User-ID`. Session
|
||||
@@ -318,7 +328,8 @@ requires (there is no DM surface; chat is per-game).
|
||||
keys are application-generated **UUIDv7**.
|
||||
- Tables: `accounts` (durable internal accounts; Stage 3 added the away-window
|
||||
columns `away_start`/`away_end` and the hint wallet `hint_balance`; Stage 6's
|
||||
migration `00005` added the `is_guest` flag for ephemeral guest rows),
|
||||
migration `00005` added the `is_guest` flag for ephemeral guest rows; Stage 9's
|
||||
migration `00007` added the `notifications_in_app_only` out-of-app push toggle),
|
||||
`identities` (platform/email/robot identities, unique `(kind, external_id)`;
|
||||
Stage 5's migration `00004` admits the `robot` kind),
|
||||
`sessions` (revoke-only opaque-token hashes), the Stage 3 game tables
|
||||
@@ -387,9 +398,16 @@ the backend and forwarded verbatim. A client that is not currently streaming fal
|
||||
back to the matchmaker's `Poll` for match-found and, for the lobby **notification
|
||||
badge** (incoming friend requests + open invitations), the client polls on lobby
|
||||
open and on focus as well as re-polling on the `notify` event — covering a push
|
||||
missed while the app was hidden. Out-of-app platform push (your-turn, nudge) is
|
||||
wired in Stage 9; session-revocation events and cursor-based stream resume are
|
||||
deferred (single-instance MVP).
|
||||
missed while the app was hidden. **Out-of-app platform push** (Stage 9) is a fallback
|
||||
the **gateway** routes from the same firehose: for an event whose recipient has **no
|
||||
live in-app stream** it resolves the backend `/internal/push-target` (their Telegram
|
||||
`external_id`, language, and the `notifications_in_app_only` flag) and asks the
|
||||
**Telegram connector** to deliver a localized message with a Mini App deep-link
|
||||
button — only when the recipient has a Telegram identity and has not confined
|
||||
notifications to the app, so the two channels never duplicate. The out-of-app set is
|
||||
your-turn, nudge, match-found and the invitation / friend-request notify sub-kinds;
|
||||
the connector renders the message and skips the rest. Session-revocation events and
|
||||
cursor-based stream resume stay deferred (single-instance MVP).
|
||||
|
||||
A separate **announcements channel** feeds the client's one-line banner (UI_DESIGN.md).
|
||||
It is a client-side **mock** rotation today; a server-driven source (operational notices,
|
||||
@@ -417,11 +435,12 @@ promotions) is future work and would deliver short markdown messages (text + lin
|
||||
| Concern | Enforced by |
|
||||
| --- | --- |
|
||||
| Public rate limiting / anti-abuse | gateway |
|
||||
| Platform credential validation, session minting | gateway |
|
||||
| Telegram initData validation (bot-token HMAC) | the Telegram connector; the gateway delegates it over gRPC, so the bot token lives only in the connector |
|
||||
| Session minting; email-code / guest validation | gateway (with backend) |
|
||||
| Session → `user_id` resolution, `X-User-ID` injection | gateway |
|
||||
| Authorisation, ownership, state transitions | backend (`X-User-ID` is the sole identity input) |
|
||||
| Admin authentication | gateway validates HTTP Basic Auth (`GATEWAY_ADMIN_*`), then reverse-proxies to backend admin endpoints |
|
||||
| backend ↔ gateway trust | the network (only gateway may reach backend) |
|
||||
| backend ↔ gateway ↔ connector trust | the network (only gateway may reach backend; the connector serves unauthenticated gRPC on the internal segment) |
|
||||
|
||||
This is an explicit, accepted MVP risk: compromise of the gateway↔backend
|
||||
network segment defeats backend authentication. Mitigated by network isolation;
|
||||
@@ -438,10 +457,15 @@ a dedicated redeem sub-limit or a longer code is the hardening step if abuse app
|
||||
|
||||
## 13. Deployment (informational)
|
||||
|
||||
Single public origin, path-routed: the UI, the gateway public surface and the
|
||||
admin surface share one host that terminates TLS. MVP runs one `gateway`, one
|
||||
`backend`, one Postgres. Docker/compose environments are introduced when there
|
||||
is something to deploy.
|
||||
Single public origin, path-routed: a mini-landing at the root, the **Telegram Mini
|
||||
App under `/telegram/`** (the gateway serves the static UI build; outside Telegram
|
||||
that path redirects to the root), the gateway public surface and the admin surface
|
||||
share one host that terminates TLS. The **Telegram connector** runs as a separate
|
||||
container with **no public ingress** — it long-polls Telegram and egresses through a
|
||||
VPN sidecar, answering only internal gRPC. MVP runs one `gateway`, one `backend`, one
|
||||
Postgres, plus the connector. The connector's Docker/compose ships now
|
||||
(`platform/telegram/deploy`, mirroring `../15-puzzle`); the full multi-service deploy
|
||||
is Stage 12.
|
||||
|
||||
## 14. CI & branches
|
||||
|
||||
|
||||
+14
-10
@@ -22,15 +22,19 @@ Settings also pick the board's bonus-label style (beginner / classic / none). A
|
||||
costs nothing when the rack has no legal move. The word-check accepts only the
|
||||
variant's alphabet, remembers answers within the session and rate-limits repeats.
|
||||
|
||||
### Identity & sessions *(Stage 1 / 6)*
|
||||
### Identity & sessions *(Stage 1 / 6 / 9)*
|
||||
A player arrives from a platform (Telegram first), via email login, or as an
|
||||
ephemeral guest. The gateway validates the credential once and mints a thin
|
||||
session token; the backend resolves it to an internal `user_id`. Guests are
|
||||
session-only with restricted features (auto-match only; no friends, stats or
|
||||
history). While the app is open the client keeps a live stream and receives
|
||||
in-app updates in real time — the opponent's move, your turn, chat, nudges and a
|
||||
found match; out-of-app push (your turn, nudge) is delivered by the platform
|
||||
later (Stage 9).
|
||||
session token; the backend resolves it to an internal `user_id`. A **Telegram Mini
|
||||
App** launch authenticates from the platform's signed `initData`, themes the UI to
|
||||
the Telegram colours, and — on first contact — seeds the new account's interface
|
||||
language from the Telegram client. Guests are session-only with restricted features
|
||||
(auto-match only; no friends, stats or history). While the app is open the client
|
||||
keeps a live stream and receives in-app updates in real time — the opponent's move,
|
||||
your turn, chat, nudges and a found match. When the app is **closed**, the chosen
|
||||
out-of-app events (your turn, nudge, a found match, an invitation or friend request)
|
||||
arrive as a **Telegram notification** instead — unless the player keeps notifications
|
||||
in the app only (a profile setting, **on by default**).
|
||||
|
||||
### Accounts, linking & merge *(Stage 1 / 10)*
|
||||
First platform contact auto-provisions a durable account. From the profile a
|
||||
@@ -42,9 +46,9 @@ account (stats summed, games/friends transferred).
|
||||
Bottom tab menu: **my games**, **profile**. Auto-match (always 2 players) joins a
|
||||
per-variant pool and is paired with the next waiting human; after 10 s with no
|
||||
human the robot substitutes (the robot arrives in Stage 5). Friend games (2–4) are
|
||||
formed by inviting players from the friend list (deep-link invites arrive with the
|
||||
platform integration): the inviter chooses the settings and the game starts once
|
||||
every invitee has accepted — any decline cancels it, and an unanswered invitation
|
||||
formed by inviting players from the friend list (an invitation, like a friend code,
|
||||
is shareable as a Telegram deep link that opens it directly): the inviter chooses the
|
||||
settings and the game starts once every invitee has accepted — any decline cancels it, and an unanswered invitation
|
||||
expires after seven days.
|
||||
|
||||
### Playing a game *(Stage 3)*
|
||||
|
||||
+14
-10
@@ -23,15 +23,19 @@ top-1 подсказку, безлимитную проверку слова с
|
||||
Проверка слова принимает только алфавит варианта, запоминает ответы в рамках сессии
|
||||
и ограничивает частоту повторов.
|
||||
|
||||
### Личность и сессии *(Stage 1 / 6)*
|
||||
### Личность и сессии *(Stage 1 / 6 / 9)*
|
||||
Игрок приходит с платформы (сначала Telegram), через email-вход или как
|
||||
эфемерный гость. Gateway один раз валидирует доступ и выдаёт тонкий
|
||||
session-токен; backend сопоставляет его с внутренним `user_id`. Гость —
|
||||
только сессия, с урезанными функциями (только авто-подбор; без друзей,
|
||||
статистики и истории). Пока приложение открыто, клиент держит живой стрим и
|
||||
получает обновления в реальном времени — ход соперника, ваш ход, чат, nudge и
|
||||
найденный матч; внеприложенческий push (ваш ход, nudge) платформа доставит
|
||||
позже (Stage 9).
|
||||
session-токен; backend сопоставляет его с внутренним `user_id`. Запуск **Telegram
|
||||
Mini App** авторизует по подписанным `initData` платформы, перекрашивает интерфейс
|
||||
в цвета Telegram и — при первом контакте — задаёт язык интерфейса нового аккаунта по
|
||||
языку Telegram-клиента. Гость — только сессия, с урезанными функциями (только
|
||||
авто-подбор; без друзей, статистики и истории). Пока приложение открыто, клиент
|
||||
держит живой стрим и получает обновления в реальном времени — ход соперника, ваш ход,
|
||||
чат, nudge и найденный матч. Когда приложение **закрыто**, выбранные внеприложенческие
|
||||
события (ваш ход, nudge, найденный матч, приглашение или заявка в друзья) приходят
|
||||
вместо этого **уведомлением в Telegram** — если только игрок не оставил уведомления
|
||||
только в приложении (настройка профиля, **включена по умолчанию**).
|
||||
|
||||
### Аккаунты, привязка и слияние *(Stage 1 / 10)*
|
||||
Первый контакт с платформы заводит постоянный аккаунт. Из профиля игрок
|
||||
@@ -43,9 +47,9 @@ session-токен; backend сопоставляет его с внутренн
|
||||
Нижнее tab-меню: **мои игры**, **профиль**. Авто-подбор (всегда 2 игрока)
|
||||
встаёт в пул по варианту и сводится со следующим ожидающим человеком; через 10 с
|
||||
без человека подставляется робот (робот — в Stage 5). Игры с друзьями (2–4)
|
||||
формируются приглашением игроков из списка друзей (приглашения по deep-link
|
||||
появятся с платформенной интеграцией): инициатор выбирает настройки, и партия
|
||||
стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
|
||||
формируются приглашением игроков из списка друзей (приглашение, как и код друга,
|
||||
можно отправить deep-link'ом в Telegram, который откроет его сразу): инициатор
|
||||
выбирает настройки, и партия стартует, когда приняли все приглашённые — любой отказ отменяет приглашение, а без
|
||||
ответа приглашение протухает через семь дней.
|
||||
|
||||
### Игровой процесс *(Stage 3)*
|
||||
|
||||
+4
-2
@@ -5,8 +5,10 @@ Visual and interaction conventions for the `ui` client. Behaviour lives in
|
||||
points this doc references) lives in [`ARCHITECTURE.md`](ARCHITECTURE.md). The client
|
||||
is **pure HTML5/CSS + Unicode** — no image/font/SVG assets; icons are CSS shapes or
|
||||
emoji glyphs. Tokens are CSS custom properties (`ui/src/app.css`), light/dark via
|
||||
`prefers-color-scheme` or an explicit Settings choice, and **Telegram-themeParams-ready**
|
||||
(the tokens can be overridden at runtime).
|
||||
`prefers-color-scheme` or an explicit Settings choice, and **Telegram-themed** (Stage 9):
|
||||
on a Telegram Mini App launch — the app is served under `/telegram/` and detects the
|
||||
launch by `Telegram.WebApp.initData` — the SDK's `themeParams` override the tokens at
|
||||
runtime; opened outside Telegram, the `/telegram/` path redirects to the site root.
|
||||
|
||||
## Layout shell (`components/Screen.svelte`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user