Перейти к основному содержимому

Slash-команды

Reason Space шифрует чат end-to-end, и бот не получает обычные сообщения пользователей как webhook — единственный inbound-канал «юзер → бот» это slash-команды.

Когда пользователь вызывает команду бота, её значения едут боту в plain-text (в обход E2E-шифрования чата) — но только по явному opt-in пользователя: вызвать команду = осознанно отправить эти данные конкретному боту. Объём строго ограничен схемой команды и суммарным размером — это структурная граница утечки.

Обычные чат-сообщения не доставляются боту никаким способом. Нет события вроде «бот получил DM» или «юзер написал боту». Inbound-текст есть только через slash-команды.

Жизненный цикл

Бот регистрирует набор команд

Сервер бота объявляет весь свой набор команд одним bulk-overwrite запросом PUT /api/bot/commands (заголовок Authorization: Bot <token>). Каждая команда — имя, описание и schema опций. См. Регистрация и Опции.

Юзер видит каталог и вызывает команду

В канале клиент подтягивает каталог доступных команд (GET /api/chat/{space_id}/{channel_id}/commands) для автокомплита. Пользователь выбирает команду, заполняет опции и вызывает её (POST /api/chat/{space_id}/{channel_id}/commands/invoke). Платформа валидирует доступ актора, доступность бота и значения опций против схемы. См. Инвокация.

Платформа шлёт боту webhook command.invoked

После валидации платформа адресно доставляет боту webhook-событие command.invoked с типизированными options, actor_user_id, channel_id и подписанным interaction_token. См. Событие и ответ.

Бот отвечает эфемерно

Бот отвечает инвокеру по interaction_token через POST /api/bot/interactions/respond. Ответ виден только вызвавшему пользователю (точечный WebSocket) и не персистится в чат.

flowchart LR
Bot["Сервер бота"] -->|"PUT /api/bot/commands"| API[Reason Space API]
User["Пользователь"] -->|"GET .../commands<br/>POST .../commands/invoke"| API
API -->|"webhook command.invoked<br/>(+interaction_token)"| Bot
Bot -->|"POST /api/bot/interactions/respond"| API
API -.->|"эфемерный ответ (WS)"| User

Требуемый scope

Регистрация, доставка и видимость команд гейтятся одним scope:

commandsscope

Право регистрировать slash-команды и принимать событие command.invoked. Выдаётся владельцем при invite бота в Space — это не параметр кода бота и не указывается в декораторе @bot.command(...).

Команду пользователь увидит в канале и сможет вызвать, только если у membership бота есть scope commands и канал входит в allowed_channel_ids. Сама регистрация (PUT /api/bot/commands) глобальна для бота; scope commands гейтит доставку и видимость в конкретном Space. См. Scopes.

Приватность

Значения опций ≤ 1024 байт plaintext. Суммарный размер типизированных значений инвокации ограничен 1024 байтами (UTF-8) — анти-«/note <дамп чата>». Превышение → ошибка валидации, событие боту не уходит.

  • Schema-bound. Юзер не может прислать боту произвольный текст — только значения, объявленные в схеме команды. Неизвестные опции отбрасываются, типы приводятся (string / integer / number / boolean / user / channel), required / choices / min / max проверяются на стороне платформы.
  • НЕ попадают в чат, ленту и модерацию. Значения инвокации передаются боту только через адресный webhook command.invoked. Они не создают сообщение в канале, не видны другим участникам и не проходят через модерацию. Эфемерный ответ бота тоже не персистится.
  • Actor из подписанного interaction-token. actor_user_id берётся из interaction_token — это HMAC-SHA256 подпись над (interaction_id, actor, channel, space, bot) с TTL 900 секунд (15 минут). Бот не может подменить инвокера: при ответе платформа проверяет подпись и что токен принадлежит именно этому боту.

В payload_summary audit log сохраняется только имя команды и число опций ({"command": ..., "n_options": ...}) — без значений. Полный plaintext опций нигде на стороне платформы не логируется.

Пример (Python SDK)

from reasonspace_bot import Bot, CommandInvokedEvent

bot = Bot() # BOT_TOKEN + WEBHOOK_SECRET из env

# Объявить набор команд при старте (bulk-overwrite).
async def setup() -> None:
await bot.client.register_commands([
{
"name": "weather",
"description": "Погода в городе",
"options": [
{"name": "city", "type": "string",
"description": "Название города", "required": True},
],
},
])

# Хендлер на конкретную команду (имя — БЕЗ слэша, БЕЗ scope= в декораторе).
@bot.command("weather")
async def on_weather(event: CommandInvokedEvent) -> None:
city = event.options.get("city", "")
# Эфемерный ответ — виден только вызвавшему пользователю.
await bot.respond(event, f"В городе {city}: ясно, +18°C")

if __name__ == "__main__":
bot.run(port=8765)

@bot.command("weather") принимает только имя команды — без слэша и без параметра scope=. Scope commands запрашивается при invite бота в Space, а не в коде.

Дальше