Пример на SDK
Полный рабочий бот целиком на slash-командах: при старте декларирует свой набор
команд через register_commands, а на каждый вызов отвечает эфемерным сообщением
(видно только инвокеру) через bot.respond.
Slash-команды — единственный inbound-канал для бота. Обычные сообщения
пользователей не доставляются боту как webhook (privacy hard-rule): content
зашифрован E2E и боту недоступен. Если бот должен реагировать на ввод человека —
делайте это через команды.
Как устроен бот на командах
bot.client.register_commands([...]) — idempotent bulk-overwrite: один вызов
переписывает весь набор команд бота. Платформа отдаёт его пользователям в
каталоге команд.
@bot.command("ping") — имя команды без /. Декоратор НЕ принимает
scope=: доступ к командам бота запрашивается scope'ом commands при
приглашении (invite), а не в коде хендлера.
Внутри хендлера — await bot.respond(event, "..."). Ответ видит только тот,
кто вызвал команду.
Полный пример
from reasonspace_bot import Bot, CommandInvokedEvent
bot = Bot() # читает BOT_TOKEN + WEBHOOK_SECRET из env
async def register() -> None:
await bot.client.register_commands(
[
{
"name": "ping",
"description": "Проверить, что бот жив",
},
{
"name": "play",
"description": "Найти и поставить трек",
"options": [
{
"name": "query",
"type": "string",
"description": "Название трека или артист",
"required": True,
}
],
},
]
)
@bot.command("ping")
async def cmd_ping(event: CommandInvokedEvent) -> None:
await bot.respond(event, "pong")
@bot.command("play")
async def cmd_play(event: CommandInvokedEvent) -> None:
query = event.options.get("query", "").strip()
if not query:
await bot.respond(event, "Укажите трек: /play <название>")
return
await bot.respond(
event,
f"Запрос «{query}» принят в канале {event.channel_id} "
f"от пользователя {event.actor_user_id}",
)
if __name__ == "__main__":
import asyncio
asyncio.run(register()) # один раз при старте — декларируем набор команд
bot.run(port=8765)
register_commands лучше вызывать один раз при деплое/старте (например, из
bootstrap-хука или отдельным CLI-скриптом), а не на каждый webhook. Это
полная перезапись набора — повторные вызовы безопасны (idempotent), но лишние.
Регистрация: что отправляется
bot.client.register_commands(commands) делает PUT /api/bot/commands с телом
{"commands": [...]} (заголовок Authorization: Bot <token>).
namestringrequiredИмя команды без ведущего /. Это ключ, по которому платформа маршрутизирует
вызов и по которому вы регистрируете хендлер: @bot.command("ping").
descriptionstringЧеловекочитаемое описание для каталога команд.
optionsarrayОпции команды. Для play — одна string-опция query.
options[].namestringrequiredИмя опции — ключ в event.options при вызове.
options[].typestringrequiredТип значения (например, string).
options[].requiredbooleanОбязательна ли опция.
Управление набором команд — три метода клиента:
| Метод SDK | HTTP | Назначение |
|---|---|---|
client.register_commands(commands) | PUT /api/bot/commands | Перезаписать весь набор |
client.list_commands() | GET /api/bot/commands | Получить текущий набор |
client.delete_command(name) | DELETE /api/bot/commands/{name} | Удалить команду (204 No Content) |
Событие command.invoked
Когда пользователь вызывает команду, платформа доставляет боту webhook-событие
типа command.invoked. SDK разбирает его в CommandInvokedEvent и направляет в
хендлер, зарегистрированный под этим именем через @bot.command(...).
commandstringИмя вызванной команды (без /). По нему SDK выбирает хендлер.
optionsdictЗначения опций: event.options["query"]. Пустой словарь, если опций нет.
channel_idstringКанал, из которого вызвана команда.
actor_user_idstringПользователь, вызвавший команду.
interaction_tokenstringТокен взаимодействия для эфемерного ответа. bot.respond(event, ...) берёт его
из события автоматически.
interaction_idstringИдентификатор взаимодействия.
command_versionintВерсия набора команд, в рамках которой произошёл вызов (может быть null).
space_idstringSpace, в котором вызвана команда (поле конверта события).
bot_user_idstringuser_id самого бота (поле конверта события).
Эфемерный ответ
await bot.respond(event, content) достаёт interaction_token из события и
вызывает client.respond(...) → POST /api/bot/interactions/respond с телом
{"interaction_token": ..., "content": ...}. Платформа отвечает 204 No Content.
Сообщение видно только инвокеру.
interaction_token подписан HMAC-SHA256 и живёт 15 минут (TTL 900 с). Если
обработка команды дольше — отвечайте быстро (например, «принято в работу»), а
итог отправляйте отдельным сообщением через bot.send_message(...).
Если хотите вызывать команды от лица пользователя
Помимо вызова из клиента Reason Space, есть пользовательский API (заголовок
Authorization: Bearer <user-token>):
GET /api/chat/{space_id}/{channel_id}/commands
POST /api/chat/{space_id}/{channel_id}/commands/invoke
Content-Type: application/json
{
"bot_user_id": "<uuid>",
"command": "play",
"options": { "query": "numb" }
}
Этот путь — для клиента/интеграции, действующей от имени пользователя; бот же
получает результат как событие command.invoked и отвечает через
bot.respond(...), как в примере выше.