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,7 +42,7 @@ independent (see ARCHITECTURE §9.1).
|
||||
| 6 | Gateway edge (Connect/FB, platform auth, sessions, push bridge, admin) | **done** |
|
||||
| 7 | UI — playable slice + UX polish (Svelte+Vite, board, lobby, chat, hint/word-check, i18n) | **done** |
|
||||
| 8 | UI — social/account/history (friends, blocks, invitations, profile edit, stats, history/GCG) | **done** |
|
||||
| 9 | Telegram integration (bot side-service, deep-link, push) | todo |
|
||||
| 9 | Telegram integration (bot side-service, deep-link, push) | **done** |
|
||||
| 10 | Admin & dictionary ops (complaint review, version reload) | todo |
|
||||
| 11 | Account linking & merge | todo |
|
||||
| 12 | Polish (observability, perf with evidence, deploy) | todo |
|
||||
@@ -605,6 +605,94 @@ Open details: deployment target/host; dashboards; load expectations.
|
||||
OS and can't be forced to match, and a select also avoids the iOS "clear" button
|
||||
that would empty a time field.
|
||||
|
||||
- **Stage 9** (interview + implementation):
|
||||
- **Connector as its own container** (interview): the Telegram side-service is a
|
||||
standalone module `platform/telegram` (binary `cmd/telegram`) holding the bot
|
||||
token **only there**; gateway and backend reach it by **unauthenticated gRPC**
|
||||
on the trusted internal network, and it egresses to `api.telegram.org` through a
|
||||
**VPN sidecar** (`deploy/docker-compose.yml`, mirroring `../15-puzzle`). Bot
|
||||
library **`github.com/go-telegram/bot`** (one new dep), **long-poll** updates.
|
||||
- **initData validation moved off the gateway** (interview): the gateway's HMAC
|
||||
validator was **relocated** into the connector (`internal/initdata`, now also
|
||||
returning `language_code`); the gateway calls `connector.ValidateInitData` over
|
||||
gRPC during `auth.telegram`. The hop is negligible (loopback gRPC, once per
|
||||
login). `GATEWAY_TELEGRAM_BOT_TOKEN` is gone; `GATEWAY_CONNECTOR_ADDR` replaces
|
||||
it. The `gateway/internal/auth` package was deleted.
|
||||
- **Connector gRPC API** (`pkg/proto/telegram/v1`, service `Telegram`): the
|
||||
generic methods are **platform-agnostic**, keyed by the identity `external_id`
|
||||
(so a future VK/MAX connector reuses them); only `ValidateInitData` is
|
||||
Telegram-specific. Methods: `ValidateInitData`, **`Notify`** (the out-of-app push
|
||||
— renders a localized message + a Mini App deep-link button from the FlatBuffers
|
||||
payload), `SendToUser` and `SendToGameChannel` (arbitrary admin messages — built
|
||||
and unit-tested now, **wired to the admin surface in Stage 10**; the game channel
|
||||
id lives only in connector config).
|
||||
- **Push = fallback, gateway-routed, de-dup by presence** (interview): the gateway
|
||||
already consumes the firehose and knows in-app presence (`push.Hub.HasSubscribers`),
|
||||
so it decides in-app vs out-of-app **atomically**: for a recipient with **no live
|
||||
in-app stream** it fetches a new backend `/internal/push-target`
|
||||
(`{external_id, language, notifications_in_app_only}`) and calls `connector.Notify`
|
||||
only when they have a Telegram identity and have **not** set the new flag. Push
|
||||
set: `your_turn`, `nudge`, `match_found`, and the `notify` sub-kinds `invitation`/
|
||||
`friend_request` (the connector skips the rest). Delivery runs in a goroutine so a
|
||||
slow connector never stalls the firehose; best-effort (no cursor resume — single
|
||||
instance, §10).
|
||||
- **Profile flag `notifications_in_app_only`** (interview, **default true** → push
|
||||
is **opt-in**): migration `00007` (+ jetgen), threaded through
|
||||
`account.Profile`/`UpdateProfile`, the REST DTOs, the fbs `Profile`/
|
||||
`UpdateProfileRequest` (default `true` in the schema so an unset field reads
|
||||
conservatively), and a Profile-screen toggle. Flagged at review: the channel is
|
||||
silent until a user turns it off.
|
||||
- **Language seeding from the platform** (discharges the Stage 8 forward-note):
|
||||
`account.ProvisionTelegram` seeds a **brand-new** account's `preferred_language`
|
||||
from the Telegram `language_code` and its display name from `first_name`/
|
||||
`username` (existing accounts untouched); the UI's `adoptSession` already adopts
|
||||
the server language when the user has not locked a locale, so no extra UI seeding
|
||||
was needed. The gateway forwards the fields from `ValidateInitData`.
|
||||
- **Mini App = `/telegram/` + guard** (interview): the gateway serves the one SPA
|
||||
build under `/telegram/` (Vite **relative base**; the hash router is
|
||||
path-agnostic). The UI detects a Telegram launch by `Telegram.WebApp.initData`,
|
||||
applies `themeParams`, authenticates via the existing `auth.telegram` op (UI
|
||||
`authTelegram` codec/client/transport/mock added), and routes the deep-link
|
||||
`start_param` (`g`/`i`/`f` → game / lobby-invitation / friend-code redeem). On the
|
||||
`/telegram/` path **without** initData it redirects to the site root. The official
|
||||
`telegram-web-app.js` loads from `index.html` (harmless outside Telegram).
|
||||
- **Deep-link scheme** (shared Go `platform/telegram/internal/deeplink` ↔ TS
|
||||
`ui/src/lib/deeplink.ts`): `g<game uuid>` / `i<invitation uuid>` / `f<6-digit
|
||||
code>` / empty = lobby. A friend-code **share-to-Telegram** link is shown when
|
||||
`VITE_TELEGRAM_LINK` is configured (**partially discharges TODO-5**; QR still
|
||||
open). The `Notify` button and the bot `/start` reply both wrap the payload as
|
||||
`<MiniAppURL>?startapp=<payload>`.
|
||||
- **Test environment** (interview nuance): the Bot API base is overridable for
|
||||
Telegram's test environment — `TELEGRAM_TEST_ENV=true` suffixes the token with
|
||||
`/test` so the client hits `/bot<token>/test/METHOD` (`TELEGRAM_API_BASE_URL`
|
||||
overrides the host for a mock/self-hosted server).
|
||||
- **Deploy groundwork** (interview): `platform/telegram/Dockerfile` (builds the
|
||||
connector standalone — drops backend/gateway and the solver replace from a copy
|
||||
of `go.work`, validated with `docker build`) + the connector-scoped compose with
|
||||
the VPN sidecar; a root `.dockerignore`. **No public ingress** for the connector
|
||||
(long-poll + sidecar egress); the host reverse proxy routes only to the gateway
|
||||
port, which serves the Mini App. The full multi-service deploy is **Stage 12**.
|
||||
- **Wire/codegen/CI**: new proto `pkg/proto/telegram/v1` (committed Go); fbs
|
||||
`Profile`/`UpdateProfileRequest` gained `notifications_in_app_only` (committed Go
|
||||
+ TS). `go.work` gains `use ./platform/telegram`; deps via `go mod edit` +
|
||||
`go work sync` (no-tidy). `go-unit.yaml` gained the `platform/**` path filter and
|
||||
builds/vets/tests `./platform/telegram/...`. UI grows to ~86 KB gzip JS (budget
|
||||
100 KB). The connector's unit tests use an httptest fake Bot API; a Playwright
|
||||
smoke drives the Mini App launch + guard with an injected `window.Telegram`.
|
||||
- **Stage 10 forward-note**: the admin surface will wire `connector.SendToUser`/
|
||||
`SendToGameChannel` (backend gains its own connector client) for operator
|
||||
broadcasts to a user and the game channel.
|
||||
- **Verification-time fixes** (caught by the CI gate): (1) the gateway transcode
|
||||
dropped `notifications_in_app_only` in four places (`ProfileResp`, `encodeProfile`,
|
||||
`profileUpdateHandler`, the `UpdateProfile` body) so the toggle never reached the
|
||||
backend — fixed, with a round-trip transcode test added. (2) The e2e suite was made
|
||||
**hermetic** (a shared `ui/e2e/fixtures.ts` blocks the real `telegram-web-app.js`):
|
||||
the render-blocking CDN `<script>` hung every page load on the CI runner, where
|
||||
telegram.org is unreachable, timing out all non-Telegram specs. (3) A pre-existing
|
||||
time-of-day flake in `TestTimeoutSweep` (the default 00:00–07:00 away window made
|
||||
the sweeper skip when CI ran with `now-1h` inside it) was made deterministic by
|
||||
clearing the test account's away window.
|
||||
|
||||
## Deferred TODOs (cross-stage)
|
||||
|
||||
- **TODO-1 — publish & version the solver.** Once `scrabble-solver` is stable,
|
||||
@@ -637,11 +725,12 @@ Open details: deployment target/host; dashboards; load expectations.
|
||||
value)` table so the UI stops duplicating it, and optionally moving tile exchange to
|
||||
letter **indices** end-to-end. Caveat (as for the dictionaries, TODO-2): the wire table
|
||||
must stay pinned to the same `rules.Alphabet` the engine uses, or indices drift.
|
||||
- **TODO-5 — QR / deep-link friend codes (owner's idea, Stage 8).** The one-time
|
||||
friend code is entered by hand today. Once the Telegram/native deep-link scheme
|
||||
exists (Stage 9), wrap a code in a deep link and render it as a QR so a friend can
|
||||
add you by scanning rather than typing. The code semantics (12 h TTL, single use,
|
||||
one active per issuer) stay as-is; only the delivery changes.
|
||||
- **TODO-5 — QR friend codes (owner's idea, Stage 8).** *Partially done in Stage 9:*
|
||||
the deep-link scheme now exists (`f<code>`, shared Go ↔ TS), the bot redeems it on
|
||||
launch, and the UI shows a **share-to-Telegram** link for an issued code when
|
||||
`VITE_TELEGRAM_LINK` is configured. **Still open:** render the link as a **QR** so a
|
||||
friend can add you by scanning rather than tapping/typing. The code semantics
|
||||
(12 h TTL, single use, one active per issuer) stay as-is; only the delivery changes.
|
||||
- **TODO-6 — smart default for the friend-game "game type" (owner's idea, Stage 8).**
|
||||
The play-with-friends form has no preselected variant today (an empty, required
|
||||
pick). Default it from the player's history (the variant they play most, from
|
||||
|
||||
Reference in New Issue
Block a user