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
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
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.
This commit is contained in:
+23
-2
@@ -824,7 +824,13 @@ the bot renders the message and skips the rest — so in-app-only sub-kinds like
|
||||
block-state sync to the blocker) never become a platform push. Operator broadcasts
|
||||
(`SendToUser` / `SendToGameChannel`, §10 admin) render in an **operator-chosen** language in
|
||||
the console; the backend calls them on the **gateway's bot-link relay**, which forwards them
|
||||
to the bot and **awaits its delivery ack** (so the console still reports delivered/not).
|
||||
to the bot and **awaits its delivery ack** (so the console still reports delivered/not). Beyond
|
||||
messages the same bot-link carries a **chat-gate control path** — a `ChatGate` command sets a user's
|
||||
write access in the moderated discussion chat and the bot's unary `ResolveChatEligibility` resolves a
|
||||
joiner's eligibility (neither renders a message; see *Moderated discussion chat* below). An optional
|
||||
**standalone promo bot** runs in the bot container (`TELEGRAM_PROMO_BOT_TOKEN`): a second bot
|
||||
answering `/start` with a URL button into the **main** bot's Mini App (`?startapp`, since a `web_app`
|
||||
button would sign initData with the promo token); it is self-contained — no bot-link, no gateway.
|
||||
Session-revocation events and cursor-based stream resume stay deferred (single-instance MVP).
|
||||
|
||||
A separate **advertising-banner** channel feeds the client's one-line strip (UI_DESIGN.md),
|
||||
@@ -987,9 +993,24 @@ revoked token would fail session resolution at the gateway *before* the gate, se
|
||||
login instead of the blocked screen). A block instantly **forfeits** every active game the player
|
||||
is in (the opponent wins, exactly as a resignation — the engine resigns off-turn) and cancels
|
||||
their open matchmaking games; a temporary block lapses automatically once its expiry passes (no
|
||||
sweeper — the gate recomputes against `now`). No operator identity is recorded (shared
|
||||
sweeper for the gate — it recomputes against `now`). No operator identity is recorded (shared
|
||||
Basic-Auth).
|
||||
|
||||
**Moderated discussion chat.** A channel's linked discussion group is gated by the Telegram bot
|
||||
(`TELEGRAM_CHAT_ID`): the group defaults to no-send, and a user may write only while they are
|
||||
**registered and neither admin-suspended nor holding the chat-only `chat_muted` role**
|
||||
(`eligible = registered AND NOT suspended AND NOT chat_muted` — the game suspension dominates). A
|
||||
single backend resolver behind `POST /api/v1/internal/chat-access` answers both directions: the
|
||||
bot's join-time `ResolveChatEligibility` (over the mTLS bot-link) grants write access to an
|
||||
eligible joiner, and a `chat_access_changed` event — emitted on a block/unblock, a `chat_muted`
|
||||
grant/revoke, or a temporary block lapsing (a dedicated `account.SuspensionSweeper`, since no
|
||||
request fires then) — drives a `ChatGate` command the gateway pushes to the bot. The bot applies it
|
||||
only to a member currently in the chat (a per-user `getChatMember` probe, since bots cannot list
|
||||
members); the signal is idempotent and is never an in-app or out-of-app message. `chat_muted` is an
|
||||
`account_roles` entry (an operator toggle in the console), so it needs no schema change. The bot
|
||||
must be an administrator in the group with the **restrict-members** right and `chat_member` in its
|
||||
allowed updates.
|
||||
|
||||
**Short numeric codes** (email confirm-codes and friend codes) are stored
|
||||
only as SHA-256 hashes and are short-lived and single-use. The unauthenticated
|
||||
email path carries a tight per-IP sub-limit (5 / 10 min); the **friend-code redeem**
|
||||
|
||||
Reference in New Issue
Block a user