Stage 15: dual Telegram bots & language-gated variants
Tests · Go / test (push) Successful in 9s
Tests · Integration / integration (push) Successful in 10s
Tests · UI / test (push) Successful in 20s
Tests · Go / test (pull_request) Successful in 8s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s

Service-agnostic refinement of the owner's idea: the sign-in service returns a
set of supported game languages with the user identity, and the lobby gates the
New Game variant choice by it (en -> English; ru -> Russian + Эрудит).

- Connector hosts two bots in one container (one per service language, each its
  own token + game channel; the same telegram_id spans both). ValidateInitData
  tries each token and returns the validating bot's service_language +
  supported_languages. Per-language config (TELEGRAM_BOT_TOKEN_EN/_RU, channels).
- supported_languages rides the Session (fbs, session-scoped, not persisted); the
  UI offers only the matching variants on New Game — gating only the START of a
  new game (auto-match + friend invite), not accept/open/play; backend does not
  enforce.
- service_language persisted (accounts.service_language, migration 00010, written
  every login, last-login-wins) and routes the user-facing Notify push back
  through the right bot (push-target coalesces with preferred_language).
- Admin SendToUser/SendToGameChannel gain an operator-chosen language selector in
  the console (unrelated to ValidateInitData).
- Non-Telegram logins carry the gateway default set
  (GATEWAY_DEFAULT_SUPPORTED_LANGUAGES, all variants).

Wire (committed regen): ValidateInitDataResponse +service_language
+supported_languages; Session +supported_languages; SendToUser/SendToGameChannel
+language. Docs (ARCHITECTURE/FUNCTIONAL/_ru/READMEs) + PLAN updated; stage marked done.
This commit is contained in:
Ilia Denisov
2026-06-05 09:35:53 +02:00
parent 23b5c3b5cc
commit e9f836db87
45 changed files with 1010 additions and 267 deletions
+44 -14
View File
@@ -48,7 +48,7 @@ independent (see ARCHITECTURE §9.1).
| 12 | Observability & performance (telemetry, metrics, guest GC) | **done** |
| 13 | Alphabet on the wire (UI alphabet-agnostic) | **done** |
| 14 | Solver & dictionary split (publish solver + scrabble-dictionary repo/artifact) | **done** |
| 15 | Dual Telegram bots & language-gated variants | todo |
| 15 | Dual Telegram bots & language-gated variants | **done** |
| 16 | Deploy infra & test contour (Dockerfiles, gateway static UI, compose, observability) | todo |
| 17 | Prod contour deploy (SSH export/import, manual after merge) | todo |
@@ -264,19 +264,20 @@ both — discharging **TODO-1** and **TODO-2**.
`BACKEND_DICT_DIR/<version>/`, `engine.OpenWithVersions`, per-game `dict_version` pin; a version is
safe to retire once no active game pins it).
### Stage 15 — Dual Telegram bots & language-gated variants *(feature; own interview)*
Scope (owner's idea, to design in detail at its own start): run **two bots in the one connector
container** — one for the English audience, one for Russian — each with its own token + game-channel id
+ service-language tag (the same Telegram user id spans both). `initData` validation tries each bot's
token in turn (none succeeds ⇒ invalid). The connector returns the **service language `en`/`ru`**;
`Notify`/`SendToUser` take a language key so the right bot delivers. The UI **gates the game-type
(variant) choice** by service language (en → English; ru → Russian + Эрудит).
Open details (own interview): which bot sends a notification for an **existing** game (game language vs
the player's service language) given one user id spans both bots; behaviour for **non-Telegram**
players (web/email/guest — ungated, or by interface language); the proto/wire changes
(`ValidateInitData` service-language field, a bot/language selector on the push RPCs); per-bot config +
tests. Engineering feedback already captured at the Stage 14 interview: the two-bots-in-one-container +
sequential validation + language-keyed routing model is sound.
### Stage 15 — Dual Telegram bots & language-gated variants *(done)*
Re-framed at its start to be **service-agnostic**: the sign-in service returns, with the user identity, a
**set of supported game languages** (subset of `{en, ru}`, ≥ 1) that gates the New Game variant choice.
Built: the connector hosts **two bots in one container** (one per service language, each its own token +
game channel; the same Telegram user id spans both); `ValidateInitData` tries each token in turn and
returns the validating bot's **`service_language`** + **`supported_languages`** set. The set rides the
`Session` (FlatBuffers, session-scoped, not persisted) and the UI offers only the matching variants on New
Game (en → English; ru → Russian + Эрудит) — gating **only** the start of a new game (auto-match + friend
invite); existing games of any language are unrestricted and the backend does not enforce. The service
language is persisted (`accounts.service_language`, migration `00010`, written on every login —
last-login-wins) and routes the user-facing out-of-app push (`Notify`) back through the right bot (falls
back to `preferred_language`). Non-Telegram logins (web/email/guest) carry the gateway default set
(`GATEWAY_DEFAULT_SUPPORTED_LANGUAGES`, all variants). Admin broadcasts (`SendToUser`/`SendToGameChannel`)
pick the bot by an **operator-chosen** language in the console — unrelated to `ValidateInitData`.
### Stage 16 — Deploy infra & test contour
Scope: the deploy machinery + the **test contour** (the bulk of the original Stage 14). Backend +
@@ -1006,6 +1007,35 @@ caddy; prod VPN; rollback.
auto-release step's `${{ github.* }}` contexts failed the Gitea workflow compile, so releases are
published manually for now (a logged follow-up).
- **Stage 15** (interview + implementation):
- **Re-framed service-agnostic** (interview): the owner kept the two-bots-in-one-container model but
generalised the language signal — the sign-in service returns a **set** of supported game languages
(subset of `{en, ru}`, ≥ 1) on the validate response, and the **UI gates** the New Game variant choice
by it. Two distinct scopes, deliberately not conflated: the **gating set** is per-session (rides the
`Session` fbs, never persisted — so the same `telegram_id` logged in through the en- and ru-bot gates
differently, which is correct), and the **routing language** is per-account.
- **Push routing resolved** (interview, the original "which bot delivers" open detail): only the
**user-facing `Notify`** carries the `en`/`ru` language from the user's **last `ValidateInitData`**,
persisted as `accounts.service_language` (migration `00010`, written every login — new and existing —
last-login-wins, read by `/internal/push-target` with a `preferred_language` fallback). It is NOT the
game's variant language. **Correction mid-interview:** the admin broadcasts `SendToUser` /
`SendToGameChannel` are admin-panel-only and unrelated to `ValidateInitData`; they pick the bot by an
**operator-chosen** language (a console `<select>`), so a `language` field was added to those two RPCs
sourced from the form, not from `service_language`.
- **Gating = UI-only, creation-only** (interview): the backend does not enforce (a valid game is
harmless, not a trust boundary); only the New Game pickers (auto-match + friend invite) filter — there
is no variant picker on accept/open/play, so those are inherently ungated. Non-Telegram logins
(web/email/guest) carry the gateway default set (`GATEWAY_DEFAULT_SUPPORTED_LANGUAGES`, default all).
- **Wire/connector**: `ValidateInitDataResponse` gained `service_language` + `supported_languages`; the
fbs `Session` gained `supported_languages:[string]`; `SendToUser`/`SendToGameChannel` gained
`language` (committed Go + TS regenerated via `make -C pkg gen` + `pnpm -C ui codegen`). The connector
config moved to **per-language** bots (`TELEGRAM_BOT_TOKEN_EN/_RU`, `TELEGRAM_GAME_CHANNEL_ID_EN/_RU`;
`TELEGRAM_MINIAPP_URL` shared; ≥ 1 token required — a breaking config change, no prod yet); the
server hosts a bot map and routes by language. The push template language now follows the routing bot
(was `preferred_language`) — a documented change. The deploy compose/Dockerfile env was updated to the
per-language vars (the full deploy stack is Stage 16). No CI workflow change (the Go and UI workflows
already span the touched modules).
## Deferred TODOs (cross-stage)
- ~~**TODO-1 — publish & version the solver.**~~ **Done in Stage 14.** `scrabble-solver` is