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;
}
