Files
scrabble-game/platform/telegram
Ilia Denisov f166ff30fe
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 12s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m20s
Stage 17 #4: enrich the out-of-app your-turn push + add game-over
The Telegram 'your turn' notification now names the opponent and recaps their last
move (voiced as the opponent: «{name}: my move — «WORD». Score 120:95» for a scoring
play; a short 'swapped / passed, your turn' otherwise), and a new game-over
notification reports the result + final score when a game ends by any path (closing
play, all-pass, resign, timeout). Scores are recipient-first (the reader's score
leads), 2-4 players (120:95:80).

- schema: YourTurnEvent gains opponent_name/last_action/last_word/score_line
  (appended, backward-compatible); new GameOverEvent{result, score_line}. Go + UI
  bindings regenerated (flatc 23.5.26 + pnpm codegen).
- backend: notify.YourTurn enriched + notify.GameOver; emitMove resolves the mover's
  name and emits per-recipient (your_turn to the next mover, game_over to every seat),
  with recipient-first score lines built in one place.
- gateway: game_over joins the out-of-app whitelist (routing.go).
- connector: render builds the enriched your_turn + game_over text per language (en/ru).
- tests: notify round-trip (enriched + game_over), emit (enriched fields + game_over to
  all seats / per-seat result), connector render (en/ru), routing; integration replay
  (play → your_turn with real name; resign → game_over) green.
- docs: ARCHITECTURE push catalog + out-of-app set, FUNCTIONAL (+ _ru), PLAN tracker.
2026-06-09 01:15:18 +02:00
..

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 §1/§3/§10/§12.

Service languages (dual bots)

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).

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 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 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 (wired in Stage 10). 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).

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 (Stage 11) 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.

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_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_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

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

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 lands with Stage 12.

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.