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); // ValidatePreCheckout answers whether a Telegram Stars pre_checkout_query may be // approved before any star is charged: the order in the invoice payload exists, is // still creditable (pending or an honoured-expired order, never one already paid), // and its amount and currency match the invoice. The bot calls it on every // pre_checkout_query and approves only on ok; a not-ok answer or a channel failure // declines the charge (fail-closed). Reusable Stars invoice links make this gate the // one place a repeat payment is stopped before money moves. Request/response. rpc ValidatePreCheckout(PreCheckoutRequest) returns (PreCheckoutResponse); // ForwardPayment delivers a completed Telegram Stars payment from the bot's durable // outbox to the gateway for crediting. The bot calls it (retrying until it gets a // response) after Telegram confirms the payment; the gateway forwards it to the // backend intake, which credits the order once, idempotent on // telegram_payment_charge_id. A response means the payment was durably handled // (credited, or recorded as unmatched) and the bot may forget the outbox row; a // transport error leaves the row for a later retry. Request/response. rpc ForwardPayment(ForwardPaymentRequest) returns (ForwardPaymentResponse); } // 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; CreateInvoiceCommand create_invoice = 6; } } // 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. result // carries a command's return value when it has one (the created invoice link for a // create_invoice command); it is empty otherwise. message Ack { string command_id = 1; bool delivered = 2; string error = 3; string result = 4; } // 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; } // CreateInvoiceCommand asks the bot to mint a Telegram Stars invoice link for a // pending order (createInvoiceLink in XTR). payload is the order id, which Telegram // echoes back in the pre_checkout_query and the successful_payment; amount is the // price in whole stars; title and description are shown on the invoice. The bot // returns the link in its Ack result. message CreateInvoiceCommand { string title = 1; string description = 2; string payload = 3; int64 amount = 4; } // PreCheckoutRequest asks whether a Stars pre_checkout_query for order_id at amount // (whole stars) in currency may be approved before the charge. message PreCheckoutRequest { string order_id = 1; int64 amount = 2; string currency = 3; } // PreCheckoutResponse is the approval answer. ok approves the charge; reason carries a // short user-facing decline message when ok is false. message PreCheckoutResponse { bool ok = 1; string reason = 2; } // ForwardPaymentRequest carries a completed Stars payment: order_id from the invoice // payload, the Telegram charge id (the idempotency key), the amount in whole stars, // and the payer's Telegram user id. message ForwardPaymentRequest { string order_id = 1; string telegram_payment_charge_id = 2; int64 amount = 3; int64 telegram_user_id = 4; } // ForwardPaymentResponse reports the durable outcome. credited is true when the order // was credited (or already had been); false means the payment was recorded but could // not be matched to a creditable order (an operator follows up). Either way the bot // may forget the outbox row — only a transport error triggers a retry. message ForwardPaymentResponse { bool credited = 1; }