feat(telegram): promo bot + channel-chat moderation gate #99

Merged
developer merged 7 commits from feature/telegram-promo-bot-chat-moderation into development 2026-06-21 15:21:23 +00:00
Owner

What

A second standalone promo bot in the bot container (answers /start with a localized message + a URL button into the main bot's Mini App) and a moderated discussion chat gate: the main bot grants write access on join when the Telegram user is registered and neither admin-suspended nor holding a new chat_muted role, and revokes/grants on the matching moderation change for a member currently in the chat.

Eligibility = registered AND NOT suspended AND NOT chat_muted (the game suspension dominates), resolved once in the backend and reached two ways: the bot's join-time unary ResolveChatEligibility over the existing mTLS bot-link, and a backend chat_access_changed event -> gateway -> ChatGate command (idempotent; a temporary-block-expiry sweeper may over-emit). The block/unblock path is guarded by getChatMember, since bots cannot list members.

Operational setup (lands with the Stage 18 prod cutover)

  • Promote the bot to admin in the discussion group with the "Ban users" right (the client label for the Bot API can_restrict_members) and set the group default to no-send.
  • New TEST_/PROD_ deploy vars: TELEGRAM_PROMO_BOT_TOKEN (secret), TELEGRAM_BOT_USERNAME, TELEGRAM_CHAT_ID; the promo link reuses the existing *_VITE_TELEGRAM_LINK as TELEGRAM_BOT_LINK. The operator toggles chat_muted from the user card's role dropdown.

Why a URL button (not web_app)

A web_app button signs initData with the sending (promo) bot's token, which the main bot's validator rejects -- so the cross-bot launch is a t.me/<bot>?startapp URL.

No schema change

chat_muted reuses the data-driven account_roles table -> no migration, no jetgen, no contour DB wipe.

Verified locally

gofmt clean; build/vet/unit for all four modules; the full backend integration suite (-tags integration); proto codegen idempotent; docker compose config valid.

## What A second **standalone promo bot** in the bot container (answers `/start` with a localized message + a URL button into the **main** bot's Mini App) and a **moderated discussion chat** gate: the main bot grants write access on join when the Telegram user is registered and neither admin-suspended nor holding a new **`chat_muted`** role, and revokes/grants on the matching moderation change for a member currently in the chat. Eligibility = `registered AND NOT suspended AND NOT chat_muted` (the game suspension dominates), resolved once in the backend and reached two ways: the bot's join-time unary `ResolveChatEligibility` over the existing mTLS bot-link, and a backend `chat_access_changed` event -> gateway -> `ChatGate` command (idempotent; a temporary-block-expiry sweeper may over-emit). The block/unblock path is guarded by `getChatMember`, since bots cannot list members. ## Operational setup (lands with the Stage 18 prod cutover) - Promote the bot to **admin** in the discussion group with the **"Ban users"** right (the client label for the Bot API `can_restrict_members`) and set the group default to **no-send**. - New `TEST_`/`PROD_` deploy vars: `TELEGRAM_PROMO_BOT_TOKEN` (secret), `TELEGRAM_BOT_USERNAME`, `TELEGRAM_CHAT_ID`; the promo link reuses the existing `*_VITE_TELEGRAM_LINK` as `TELEGRAM_BOT_LINK`. The operator toggles `chat_muted` from the user card's role dropdown. ## Why a URL button (not web_app) A `web_app` button signs `initData` with the *sending* (promo) bot's token, which the main bot's validator rejects -- so the cross-bot launch is a `t.me/<bot>?startapp` URL. ## No schema change `chat_muted` reuses the data-driven `account_roles` table -> no migration, no jetgen, no contour DB wipe. ## Verified locally gofmt clean; build/vet/unit for all four modules; the full backend integration suite (`-tags integration`); proto codegen idempotent; `docker compose config` valid.
developer added 1 commit 2026-06-21 12:47:32 +00:00
feat(telegram): promo bot + channel-chat moderation gate
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
e71e40eef5
Add a second standalone promo bot to the bot container (answers /start with a
localized message + a URL button into the main bot's Mini App) and gate write
access in a channel's linked discussion chat: grant on join when the Telegram
user is registered and neither admin-suspended nor holding a new chat_muted
role, and revoke/grant on the matching moderation change for a member currently
in the chat.

Eligibility (registered AND NOT suspended AND NOT chat_muted; the game
suspension dominates) is resolved once in the backend and reached two ways: the
bot's join-time unary ResolveChatEligibility over the existing mTLS bot-link,
and a backend chat_access_changed event -> gateway -> ChatGate command
(idempotent; a temporary-block-expiry sweeper may over-emit). The bot guards the
block/unblock path with getChatMember, since bots cannot list members.

A web_app button cannot open another bot's Mini App (it signs initData with the
sending bot's token), so the promo button is a t.me ?startapp URL reusing the
UI's VITE_TELEGRAM_LINK. The bot must be a chat admin with the restrict-members
right and chat_member in its allowed updates.

No schema change: chat_muted reuses the data-driven account_roles table.
developer added 1 commit 2026-06-21 12:50:11 +00:00
fix(telegram): keep a failed promo-bot construction non-fatal
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
b22b624d28
tgbot.New validates the token with getMe, so a bad or unreachable promo token
would otherwise return an error from run() and crash-loop the whole bot process
— taking the main game bot down with it, since they share the container. Log it
and skip the promo bot instead; the main bot and bot-link are unaffected.
developer added 1 commit 2026-06-21 13:19:23 +00:00
feat(telegram): chat-gate observability + grant on first registration
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m0s
a404513037
Two follow-ups from a contour test where a user joined the chat, then
registered, and got no write access — with silent logs.

Observability: log every chat_member update (chat id, configured id, user,
old->new status), the eligibility result and the grant outcome; plus a startup
self-check that warns loudly when the bot is not an administrator in the chat
with the restrict-members ("Ban users") right — the common misconfiguration,
previously invisible in the logs.

Grant on first registration: a user who joins the moderated chat BEFORE
registering is covered by no chat_member event, so the join-time grant never
fires for them. ProvisionTelegram now reports first contact, and the Telegram
auth handler emits chat_access_changed on it, so the gateway re-evaluates and
grants write access if the user is already in the chat.
developer added 1 commit 2026-06-21 14:12:59 +00:00
fix(telegram): grant write to restricted members in default-deny chats
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m22s
380f82438c
A default-deny discussion group reports a present or freshly joined member as
`restricted` (no send right), not `member`. The join filter required `member`,
so the real case never matched and a registered user stayed muted. Grant any
eligible in-chat member (member or restricted) that still lacks the send right,
with a loop guard (skip when send is already allowed) so the bot's own grant
does not re-fire. Revoking a now-ineligible user stays the chat-gate path's job,
so this never fights a chat_muted/block.
developer added 1 commit 2026-06-21 14:25:58 +00:00
fix(telegram): grant in-chat members regardless of reported can_send
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m18s
0ab1719ee9
The CanSendMessages loop-guard skipped exactly the stuck case — a restricted
member whose chat_member event reports can_send=true yet who cannot actually
write. Replace it with a precise loop guard (skip only the bot's own restrict
action, i.e. the update whose performer is the bot) and grant any eligible
in-chat member (member or restricted) otherwise. Also log the new member's
can_send, is_member and the actor id for full visibility.
developer added 1 commit 2026-06-21 14:50:46 +00:00
fix(telegram): invert the chat gate — mute the ineligible (default-allow)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
bdd1cc7d85
Telegram intersects the chat default with each user's permissions, so a per-user
grant can never exceed a deny-by-default group: the original default-deny +
grant design could not let any user write (can_send=true was AND-ed with the
denying default). Invert it — the chat allows sending by default and the bot
MUTES an ineligible member (unregistered, admin-suspended, or chat_muted) and
restores an eligible one it had muted, acting only when the current state
differs (idempotent, no self-loop). The block/unblock/chat_muted/registration
path already sets can_send to the eligibility, so it is unchanged.
owner approved these changes 2026-06-21 15:02:48 +00:00
Dismissed
developer added 1 commit 2026-06-21 15:15:12 +00:00
docs(telegram): invert chat-gate strategy in docs; tune logs; i18n text
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 57s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m15s
1ba789a1f1
- Bake the final default-allow + mute-the-ineligible strategy into
  docs/ARCHITECTURE.md, docs/FUNCTIONAL.md (+_ru), platform/telegram/README.md,
  the deploy compose comment and the PRERELEASE tracker. The live test proved a
  per-user grant cannot exceed a deny-by-default group (Telegram intersects the
  chat default with the per-user permission), so the chat allows sending by
  default and the bot restricts the ineligible instead of granting the eligible.
- Lower the per-event chat_member trace and eligibility evaluation to Debug;
  keep the actual mute/unmute actions, the startup self-check and warnings at
  Info, so prod logs only what the bot did.
- Update game.searchingForOpponent (Searching -> Waiting for opponent / Поиск ->
  Ждём соперника) and the quickmatch e2e assertions to match.
developer dismissed owner's review 2026-06-21 15:15:12 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

owner approved these changes 2026-06-21 15:21:00 +00:00
developer merged commit fa8abf22db into development 2026-06-21 15:21:23 +00:00
developer deleted branch feature/telegram-promo-bot-chat-moderation 2026-06-21 15:21:23 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#99