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

Событие и ответ

Slash-команды — единственный способ для пользователя обратиться к боту напрямую. Обычные сообщения юзеров боту как webhook не доставляются (content зашифрован E2E, бот его не читает). Когда юзер вызывает команду, платформа шлёт боту webhook-событие command.invoked и ждёт эфемерного ответа — видимого только инвокеру и не попадающего в чат или ленту.

Цепочка целиком: юзер вызывает команду через POST /api/chat/{space_id}/{channel_id}/commands/invoke → платформа эмитит command.invoked адресно целевому боту → бот отвечает через POST /api/bot/interactions/respond → ответ точечно уходит инвокеру по WebSocket. См. регистрацию команд для декларации реестра.

Событие command.invoked

Это channel-scoped событие. В отличие от broadcast-событий вроде message.created, оно доставляется только одному боту — тому, чья команда была вызвана. Поле data.target_bot_user_id гейтит доставку: если на command.invoked подписаны несколько ботов в канале, событие всё равно получит только адресат. Иначе аргументы команды утекли бы в чужих ботов.

{
"id": "deedccc1-...", // UUID delivery (idempotency)
"type": "command.invoked",
"space_id": "019bfa33-...",
"delivered_at": "2026-05-27T13:00:00Z",
"bot_user_id": "e8e311f6-...",
"data": {
"target_bot_user_id": "e8e311f6-...", // адресная доставка
"interaction_id": "7c1f...",
"interaction_token": "eyJ...base64....hex_hmac",
"channel_id": "019c3e64-...",
"actor_user_id": "09a5df2b-...",
"command": "play",
"options": { "track": "numb", "shuffle": true },
"command_version": 3,
"created_at": "2026-05-27T13:00:00Z"
}
}

Поля data

target_bot_user_idUUID

ID бота-адресата. Совпадает с bot_user_id конверта. Платформа использует его, чтобы доставить событие только целевому боту, а не всем подписчикам канала.

interaction_idUUID

Уникальный id инвокации. Возвращается юзеру синхронно в ответе на invoke и присутствует в эфемерном ответе — для корреляции на клиенте.

interaction_tokenstring

Подписанный stateless-токен (HMAC-SHA256). Бот возвращает его в respond-эндпоинт, чтобы ответить инвокеру. TTL — 900 секунд (15 минут). Сервер не хранит инвокацию в каком-либо хранилище — весь контекст (кто/где/какой бот) зашит в сам токен. См. ниже.

channel_idUUID

Канал, в котором вызвана команда.

actor_user_idUUID

ID пользователя, вызвавшего команду (инвокер). Именно ему уйдёт эфемерный ответ.

commandstring

Имя команды (без /), например play. Совпадает с зарегистрированным именем.

optionsobject

Аргументы команды — уже типизированные и провалидированные сервером по схеме команды ({option_name: typed_value}). Типы приведены: integer → int, number → float, boolean → bool, user/channel → UUID-строка. Неизвестные опции отброшены (forward-compat). Required-опции гарантированно присутствуют. Суммарный plaintext-объём значений ограничен 1024 байтами.

command_versioninteger | null

Версия конкретной команды на момент вызова. Новая команда стартует с version=1. При bulk-overwrite через PUT /api/bot/commands версия инкрементируется только если изменились её description или options; идемпотентный повторный PUT с тем же определением версию не меняет.

created_atISO 8601 timestamp

Момент инвокации (UTC).

Значения options едут plaintext — в обход E2E, по явному opt-in юзера. Это структурная граница утечки: объём строго ограничен схемой команды и суммарным размером (1024 байта). Не проектируйте команды как способ «выкачать» произвольный текст из чата.

Эфемерный ответ

В ответ на command.invoked бот шлёт POST /api/bot/interactions/respond с interaction_token и content. Ответ:

  • уходит точечно инвокеру (actor_user_id) по WebSocket (broadcast_to_user), а не в канал;
  • помечен ephemeral: true — виден только вызвавшему юзеру;
  • не персистится в чат, ленту или модерацию.
interaction_tokenstringrequired

Токен из data.interaction_token события. 1..4096 символов.

contentstringrequired

Текст ответа. 1..2000 символов.

Успех — 204 No Content. Ошибки:

КодКогда
403Токен невалиден, просрочен (>15 мин) или принадлежит другому боту
422Нарушение длины content (1..2000) или interaction_token
POST /api/bot/interactions/respond HTTP/1.1
Host: api.reasonspace.ru
Authorization: Bot bot_<48hex>
Content-Type: application/json

{
"interaction_token": "eyJ...base64....hex_hmac",
"content": "▶️ Играю numb"
}
HTTP/1.1 204 No Content

Что приходит инвокеру по WebSocket

{
"type": "interaction:response",
"interaction_id": "7c1f...",
"channel_id": "019c3e64-...",
"space_id": "019bfa33-...",
"bot_user_id": "e8e311f6-...",
"content": "▶️ Играю numb",
"ephemeral": true,
"is_bot": true
}

Эфемерный ответ — опциональный. Команда может ничего не вернуть юзеру и, например, отправить обычное сообщение в канал через POST /api/bot/spaces/{space_id}/channels/{channel_id}/messages (требует scope messages.send). Эфемерный путь — для приватных подтверждений и ошибок, которые не нужны всему каналу.

interaction-token

Токен — это base64url(payload).hex(hmac_sha256). Сервер подписывает контекст инвокации секретом BOT_TOKEN_SECRET и кладёт токен в событие. Хранилища нет — при respond сервер заново проверяет подпись, срок и принадлежность боту.

exp = now + 900 секунд

Токен живёт 15 минут. После — 403 invalid or expired interaction. Отвечайте быстро.

Привязан к боту

Если respond прислан с токеном чужой инвокации — 403 interaction does not belong to this bot.

Stateless — не single-use

Сервер не дедуплицирует и не помечает токен использованным: пока он не истёк, его подпись валидна и respond с ним пройдёт. По смыслу же одна инвокация — это один ответ инвокеру; не переиспользуйте токен намеренно.

SDK: @bot.command и bot.respond

В Python SDK команда обрабатывается декоратором @bot.command(name) — имя без /. Эфемерный ответ — через await bot.respond(event, content), токен SDK достаёт из события сам.

from reasonspace_bot import Bot, CommandInvokedEvent

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

@bot.command("play")
async def on_play(event: CommandInvokedEvent) -> None:
track = event.options.get("track")
if not track:
await bot.respond(event, "Укажите трек: /play <название>")
return

# ... запустить воспроизведение ...

# Эфемерное подтверждение — видит только инвокер
await bot.respond(event, f"▶️ Играю {track}")

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

@bot.command("play") — имя без / и без параметра scope=. Доступ к командам в конкретном space даётся scope commands при invite бота, а не в декораторе. Подробнее — scopes.

Событие command.invoked приходит в SDK как CommandInvokedEvent:

@bot.on("command.invoked")
async def any_command(event: CommandInvokedEvent) -> None:
print(event.command) # "play"
print(event.options) # {"track": "numb", "shuffle": True}
print(event.actor_user_id) # инвокер
print(event.command_version) # 3
# event.interaction_token — для bot.respond(...)

@bot.command(name) срабатывает в дополнение к общему @bot.on("command.invoked"): сначала отрабатывают все хендлеры on(...), затем — специфичный для имени команды. Упавший хендлер логируется, но не валит доставку (бот всё равно вернёт 200 на webhook).

Отвечайте на command.invoked синхронно в рамках обработки события: токен живёт 15 минут, но юзер ждёт ответ сразу. Если работа долгая — пошлите быстрый эфемерный ответ («принято»), а результат отправьте отдельным сообщением в канал.