6e03ce0131
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s
Accept real money via Telegram Stars (XTR) — the third intake rail alongside Robokassa (direct) and VK Votes. Only the bot reaches Telegram, so the rail funnels through the reverse mTLS bot-link: - the gateway mints the invoice on a CreateInvoice command (the bot calls createInvoiceLink, XTR; the link goes to WebApp.openInvoice); - the bot gates each pre_checkout_query via a ValidatePreCheckout unary (the order must exist, be still creditable and not already paid — the reusable-invoice double-pay guard; the decline reason is localised to the order account's language); - a completed successful_payment is queued in a durable pure-Go SQLite outbox and forwarded via a ForwardPayment unary, credited once (idempotent on telegram_payment_charge_id, honours an expired order), re-driven on restart and every 30s. The rail is wired by TELEGRAM_STARS_OUTBOX_DIR (default /data) but stays inert until a chip pack carries an XTR price, so seeding a Stars price in the admin is the go-live. Tests: backend integration (order->forward->credit once, duplicate, pre_checkout gate) + bot outbox unit (idempotent, restart re-drive) + executor createInvoice. Docs: PAYMENTS(+ru) §9, ARCHITECTURE, the platform/telegram README, PLAN.
169 lines
7.3 KiB
Protocol Buffer
169 lines
7.3 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);
|
|
|
|
// 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;
|
|
}
|