57c778f9b2
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.
107 lines
5.5 KiB
Markdown
107 lines
5.5 KiB
Markdown
# scrabble/platform/telegram — Telegram connector
|
|
|
|
The Telegram platform side-service. It is the **only** component that holds the bot
|
|
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.
|
|
|
|
## Single bot
|
|
|
|
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
|
|
|
|
- **Mini App auth.** `ValidateInitData` verifies Telegram Web App `initData` (HMAC
|
|
under the bot token) and returns the user identity. The gateway calls it during
|
|
the `auth.telegram` edge operation, then provisions the session through the
|
|
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 **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 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
|
|
future VK / MAX connector can implement the same service; only `ValidateInitData` is
|
|
Telegram-specific.
|
|
|
|
## gRPC API
|
|
|
|
`pkg/proto/telegram/v1`, service `Telegram`: `ValidateInitData`,
|
|
`ValidateLoginWidget`, `Notify`, `SendToUser`, `SendToGameChannel`. Generated Go is
|
|
committed under `pkg`. `ValidateLoginWidget` verifies Telegram **Login
|
|
Widget** web sign-in data — HMAC under `SHA-256(bot_token)`, distinct from initData
|
|
(`internal/loginwidget`) — for attaching a Telegram identity to an account from a
|
|
browser.
|
|
|
|
## Deep-link scheme
|
|
|
|
Shared verbatim with the UI (`ui/src/lib/deeplink.ts`). A Mini App start parameter
|
|
is a one-character kind prefix plus a value:
|
|
|
|
| Parameter | Destination |
|
|
| --- | --- |
|
|
| `g<game uuid>` | open that game |
|
|
| `i<invitation uuid>` | open that invitation |
|
|
| `f<6-digit code>` | redeem that friend code |
|
|
| empty / unknown | the lobby |
|
|
|
|
The bot turns a `/start <payload>` or a notification target into a launch-button URL
|
|
`<MiniAppURL>?startapp=<payload>`.
|
|
|
|
## Configuration
|
|
|
|
| Env var | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `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`) |
|
|
| `TELEGRAM_LOG_LEVEL` | `info` | zap log level |
|
|
| `TELEGRAM_SERVICE_NAME` | `scrabble-telegram` | OpenTelemetry `service.name` |
|
|
| `TELEGRAM_OTEL_TRACES_EXPORTER` | `none` | `none`, `stdout` or `otlp` (gRPC; endpoint from `OTEL_EXPORTER_OTLP_*`) |
|
|
| `TELEGRAM_OTEL_METRICS_EXPORTER` | `none` | `none`, `stdout` or `otlp` |
|
|
|
|
The **test environment** is selected by `TELEGRAM_TEST_ENV=true`, which suffixes the
|
|
Bot API path with `/test` (the connector appends it to the token, since the client
|
|
builds `<host>/bot<token>/<method>`).
|
|
|
|
## Build, test, run
|
|
|
|
```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
|
|
```
|
|
|
|
## Deploy
|
|
|
|
The connector runs in its **own container** with the bot token held only there and
|
|
all egress through a VPN sidecar (`deploy/docker-compose.yml`, mirroring
|
|
`../../15-puzzle`). It needs no public ingress — it long-polls Telegram and answers
|
|
internal gRPC at `telegram:9091` on the shared `edge` network. The host reverse proxy
|
|
routes public traffic to the **gateway** port only, which serves the Mini App under
|
|
`/telegram/`. The full multi-service deploy is `deploy/docker-compose.yml`.
|
|
|
|
A real end-to-end Telegram smoke needs a BotFather bot, its token, a public HTTPS
|
|
Mini App origin, and the connector container; the unit tests cover the wire format,
|
|
templates, deep-links and the gRPC handlers without a live bot.
|