e71e40eef5
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.
102 lines
4.2 KiB
Protocol Buffer
102 lines
4.2 KiB
Protocol Buffer
syntax = "proto3";
|
|
|
|
// Package scrabble.botlink.v1 is the reverse control channel between a remote
|
|
// Telegram bot and the gateway. The bot dials the gateway and opens a single
|
|
// long-lived Link stream (mTLS); once the stream is open the gateway pushes send
|
|
// Commands down it and the bot returns one Ack per command. This keeps the bot
|
|
// egress (the Bot API token, getUpdates long-poll and sendMessage) off the main
|
|
// host with no inbound port on the bot. See docs/ARCHITECTURE.md.
|
|
package scrabble.botlink.v1;
|
|
|
|
option go_package = "scrabble/pkg/proto/botlink/v1;botlinkv1";
|
|
|
|
import "telegram/v1/telegram.proto";
|
|
|
|
// BotLink is the reverse (bot-dials-gateway) control channel. The bot is the gRPC
|
|
// client; once the stream is open the gateway (server) sends Commands at will and
|
|
// the bot returns one Ack per command. Delivery is best-effort, at-most-once: a
|
|
// command lost across a reconnect is not replayed.
|
|
service BotLink {
|
|
// Link opens the single bot <-> gateway stream. The first client message is
|
|
// Hello; thereafter the client sends one Ack per received Command.
|
|
rpc Link(stream FromBot) returns (stream ToBot);
|
|
|
|
// ResolveChatEligibility answers whether the Telegram user identified by
|
|
// external_id may write in the moderated discussion chat: registered with an
|
|
// account and neither admin-suspended nor chat-muted. The bot calls it over the
|
|
// same mTLS channel when a user joins the chat, to decide whether to grant the
|
|
// write permission. Delivery of the answer is request/response (not best-effort).
|
|
rpc ResolveChatEligibility(ChatEligibilityRequest) returns (ChatEligibilityResponse);
|
|
}
|
|
|
|
// FromBot is a message the bot sends to the gateway: the opening Hello, then one
|
|
// Ack per Command.
|
|
message FromBot {
|
|
oneof msg {
|
|
Hello hello = 1;
|
|
Ack ack = 2;
|
|
}
|
|
}
|
|
|
|
// ToBot is a message the gateway sends to the bot. Only Command is carried today.
|
|
message ToBot {
|
|
Command command = 1;
|
|
}
|
|
|
|
// Hello registers the bot on connect. instance_id identifies the bot process for
|
|
// gateway-side logging and metrics; owns_updates reports whether this bot runs the
|
|
// exclusive getUpdates long-poll (exactly one bot must, else Telegram returns 409).
|
|
message Hello {
|
|
string instance_id = 1;
|
|
bool owns_updates = 2;
|
|
}
|
|
|
|
// Command is one send instruction addressed by command_id, which the bot echoes in
|
|
// its Ack. Exactly one payload is set; the payloads reuse the connector request
|
|
// shapes from scrabble.telegram.v1.
|
|
message Command {
|
|
string command_id = 1;
|
|
oneof payload {
|
|
scrabble.telegram.v1.NotifyRequest notify = 2;
|
|
scrabble.telegram.v1.SendToUserRequest send_to_user = 3;
|
|
scrabble.telegram.v1.SendToGameChannelRequest send_to_channel = 4;
|
|
ChatGateCommand chat_gate = 5;
|
|
}
|
|
}
|
|
|
|
// Ack reports the outcome of the Command with command_id. delivered mirrors the
|
|
// connector delivery semantics (false when the kind is not rendered out-of-app, the
|
|
// user never started the bot, or no channel is configured); error carries an
|
|
// unexpected transport/render failure, distinct from a clean not-delivered.
|
|
message Ack {
|
|
string command_id = 1;
|
|
bool delivered = 2;
|
|
string error = 3;
|
|
}
|
|
|
|
// ChatGateCommand sets a Telegram user's write access in the moderated discussion
|
|
// chat. external_id is the user's Telegram identity (as in the backend identities
|
|
// table); allow grants the right to write when true and revokes it when false. The
|
|
// bot applies it only to a user currently in the chat — it guards on getChatMember,
|
|
// so a command for an absent user is a no-op. The gateway emits one whenever the
|
|
// user's eligibility may have changed: an admin block or unblock, a chat_muted
|
|
// grant or revoke, or a temporary block lapsing.
|
|
message ChatGateCommand {
|
|
string external_id = 1;
|
|
bool allow = 2;
|
|
}
|
|
|
|
// ChatEligibilityRequest asks whether the Telegram user identified by external_id
|
|
// may write in the moderated discussion chat.
|
|
message ChatEligibilityRequest {
|
|
string external_id = 1;
|
|
}
|
|
|
|
// ChatEligibilityResponse is the eligibility answer. registered reports whether the
|
|
// external_id maps to an account at all; eligible is the final gate the bot acts on
|
|
// (registered and neither admin-suspended nor chat-muted).
|
|
message ChatEligibilityResponse {
|
|
bool registered = 1;
|
|
bool eligible = 2;
|
|
}
|