Событие и ответ
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_idUUIDID бота-адресата. Совпадает с bot_user_id конверта. Платформа использует
его, чтобы доставить событие только целевому боту, а не всем подписчикам
канала.
interaction_idUUIDУникальный id инвокации. Возвращается юзеру синхронно в ответе на invoke
и присутствует в эфемерном ответе — для корреляции на клиенте.
interaction_tokenstringПодписанный stateless-токен (HMAC-SHA256). Бот возвращает его в respond-эндпоинт, чтобы ответить инвокеру. TTL — 900 секунд (15 минут). Сервер не хранит инвокацию в каком-либо хранилище — весь контекст (кто/где/какой бот) зашит в сам токен. См. ниже.
channel_idUUIDКанал, в котором вызвана команда.
actor_user_idUUIDID пользователя, вызвавшего команду (инвокер). Именно ему уйдёт эфемерный ответ.
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 сервер заново проверяет подпись, срок и
принадлежность боту.
Токен живёт 15 минут. После — 403 invalid or expired interaction.
Отвечайте быстро.
Если respond прислан с токеном чужой инвокации — 403 interaction does not belong to this bot.
Сервер не дедуплицирует и не помечает токен использованным: пока он не истёк, его подпись валидна и 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 минут, но юзер ждёт ответ сразу. Если работа долгая — пошлите
быстрый эфемерный ответ («принято»), а результат отправьте отдельным
сообщением в канал.