feat(telegram,game): single bot + per-user variant preferences
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 57s

Collapse the two per-language Telegram bots into one unified bot and
replace language-based variant gating with explicit per-user variant
preferences.

- Telegram: one bot; drop service_language and the supported_languages
  set everywhere (DB, account, auth, FlatBuffers Session wire, gateway,
  connector proto). The single bot renders chat and out-of-app push in
  the recipient's preferred_language; remove the game-language push
  routing override (notify Intent.Language / push Event.language).
- Preferences: new accounts.variant_preferences (text[], DB default
  {erudit_ru}, CHECK non-empty + subset of the three variants). Gates
  the New Game picker, vs-AI and the friend invitation the player
  creates, enforced server-side (HTTP 400 otherwise); an invited friend
  may still accept any variant. Edited on the Settings screen; variants
  are Erudit-first everywhere.
- Admin: drop the per-bot language selectors (broadcast / send-to-user)
  and the feedback channel_lang column/field.
- Env/CI: collapse TELEGRAM_BOT_TOKEN_{EN,RU}, TELEGRAM_GAME_CHANNEL_ID_{EN,RU},
  VITE_TELEGRAM_LINK{,_EN,_RU} and VITE_TELEGRAM_GAME_CHANNEL_NAME_{EN,RU}
  to single unsuffixed names; drop GATEWAY_DEFAULT_SUPPORTED_LANGUAGES.
- Docs updated (ARCHITECTURE, FUNCTIONAL + _ru, platform/telegram, gateway,
  backend, ui, UI_DESIGN, PRERELEASE).

The migration squash is deferred to a follow-up PR.
This commit is contained in:
Ilia Denisov
2026-06-20 14:08:27 +02:00
parent 1933849dba
commit 57c778f9b2
99 changed files with 1006 additions and 1256 deletions
+21 -25
View File
@@ -1,22 +1,20 @@
# scrabble/platform/telegram — Telegram connector
The Telegram platform side-service. It is the **only** component that holds the bot
tokens: it runs a Bot API long-poll loop **per service language** (Mini App launch +
deep-links) and serves the connector gRPC API that the gateway and backend call over
the trusted internal network. See
[`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md) §1/§3/§10/§12.
token: it runs a Bot API long-poll loop (Mini App launch + deep-links) and serves
the connector gRPC API that the gateway and backend call over the trusted internal
network. See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md) §1/§3/§10/§12.
## Service languages (dual bots)
## Single bot
The connector hosts **one bot per service language** (`en`, `ru`) — each its own
token + game channel, configured by the `*_EN` / `*_RU` env vars; at least one is
required. The **same Telegram user id spans both bots**. `ValidateInitData` tries
each bot's token in turn (none validates ⇒ invalid) and reports which bot validated:
its **`service_language`** (persisted by the backend to route the user's later push)
and its **`supported_languages`** set (which the UI gates the New Game variant choice
by — `en` → English, `ru` → Russian + Эрудит). The user-facing `Notify` routes by the
recipient's persisted service language; the admin `SendToUser` / `SendToGameChannel`
route by an **operator-chosen** `language` (unrelated to login).
The connector hosts **one unified bot** — one token plus one optional game channel,
configured by `TELEGRAM_BOT_TOKEN` and `TELEGRAM_GAME_CHANNEL_ID`. `ValidateInitData`
validates `initData` against that single token (it does not validate ⇒ invalid) and
returns only the Telegram user identity — there is no per-bot service language and no
supported-languages set. Every message the bot sends is rendered in the **recipient's
interface language**: the user-facing `Notify` renders in the request's `language` (the
recipient's interface language); the admin `SendToUser` / `SendToGameChannel` render in
an **operator-chosen** `language`. No call routes between bots — there is only one.
## Responsibilities
@@ -26,17 +24,17 @@ route by an **operator-chosen** `language` (unrelated to login).
backend internal API — so the bot token never leaves this process.
- **Out-of-app push.** `Notify` renders a backend push event (your_turn, nudge,
match_found, and the invitation / friend_request notify sub-kinds) into a
localized message with a Mini App launch button and sends it **through the bot for
the request's `language`** (the recipient's service language). The gateway calls it
**only** for a recipient with no live in-app stream and the
localized message with a Mini App launch button and sends it through the **single
bot**, rendered in the request's `language` (the recipient's interface language).
The gateway calls it **only** for a recipient with no live in-app stream and the
`notifications_in_app_only` flag off, so the platform push never duplicates in-app
delivery.
- **Bot chat.** `/start <payload>` (and the chat menu button) reply with a Mini App
launch button; a deep-link payload routes the launch to a game / invitation /
friend code.
- **Admin messaging.** `SendToUser` and `SendToGameChannel` send
arbitrary text to one user or a game channel through the bot the request selects by
`language` (an operator choice in the admin console).
arbitrary text to one user or a game channel through the single bot, rendered in the
`language` the operator chooses in the admin console.
The generic methods (`Notify`, `SendToUser`, `SendToGameChannel`) address a
recipient by the identity `external_id` (as in the backend `identities` table), so a
@@ -71,11 +69,9 @@ The bot turns a `/start <payload>` or a notification target into a launch-button
| Env var | Default | Meaning |
| --- | --- | --- |
| `TELEGRAM_BOT_TOKEN_EN` | — | English bot's API token + initData HMAC secret |
| `TELEGRAM_BOT_TOKEN_RU` | — | Russian bot's API token + initData HMAC secret (≥ 1 of EN/RU required) |
| `TELEGRAM_GAME_CHANNEL_ID_EN` | — | English bot's game channel chat id for `SendToGameChannel` |
| `TELEGRAM_GAME_CHANNEL_ID_RU` | — | Russian bot's game channel chat id |
| `TELEGRAM_MINIAPP_URL` | — (required) | Mini App HTTPS origin, shared by all bots (BotFather-registered) |
| `TELEGRAM_BOT_TOKEN` | — (required) | The bot's API token + initData HMAC secret |
| `TELEGRAM_GAME_CHANNEL_ID` | — | The bot's game channel chat id for `SendToGameChannel` |
| `TELEGRAM_MINIAPP_URL` | — (required) | Mini App HTTPS origin (BotFather-registered) |
| `TELEGRAM_GRPC_ADDR` | `:9091` | connector gRPC listen address |
| `TELEGRAM_API_BASE_URL` | `https://api.telegram.org` | Bot API host override (mock / self-hosted) |
| `TELEGRAM_TEST_ENV` | `false` | route to the Bot API **test environment** (`/bot<token>/test/METHOD`) |
@@ -93,7 +89,7 @@ builds `<host>/bot<token>/<method>`).
```sh
go build ./platform/telegram/...
go test ./platform/telegram/... # unit tests use an httptest fake Bot API
go run ./platform/telegram/cmd/telegram # needs a real TELEGRAM_BOT_TOKEN_EN or _RU
go run ./platform/telegram/cmd/telegram # needs a real TELEGRAM_BOT_TOKEN
```
## Deploy