Инвокация (юзер)
Команду бота вызывает пользователь из канала — это делает клиент Reason Space, но контракт открыт и описан ниже. Платформа предоставляет два Bearer-эндпоинта: каталог доступных команд (для автокомплита) и сам вызов.
Это user-facing часть цепочки (Bearer-токен пользователя), не bot-API. Регистрация набора команд и приём события — на стороне бота: см. Регистрацию и Событие и ответ. Slash-команды — единственный inbound-канал «юзер → бот»: обычные сообщения юзеров боту как webhook не доставляются (чат E2E-encrypted).
Каталог команд для автокомплита
GET /api/chat/{space_id}/{channel_id}/commands возвращает список команд,
доступных пользователю в этом канале. Клиент использует его для автокомплита
при вводе /.
space_idUUIDrequiredSpace, в котором находится канал.
channel_idUUIDrequiredКанал, для которого собирается каталог.
Аутентификация — Authorization: Bearer <user_token>. Актор должен видеть
канал (can_see_room), иначе 403.
В каталог попадает команда бота, только если для этого Space у membership бота одновременно:
- есть membership в Space;
- канал входит в
allowed_channel_ids(доступ к каналу); - выдан scope
commands.
curl https://api.reasonspace.ru/api/chat/{space_id}/{channel_id}/commands \
-H "Authorization: Bearer <user_token>"
Ответ
Массив элементов каталога:
[
{
"bot_user_id": "e8e311f6-...",
"name": "weather",
"description": "Погода в городе",
"options": [
{
"name": "city",
"type": "string",
"description": "Название города",
"required": true
}
],
"version": 3
}
]
bot_user_idstringID бота, которому принадлежит команда. Передаётся обратно при вызове.
namestringИмя команды без / (например, weather).
descriptionstringЧеловекочитаемое описание команды.
optionsarrayСхема опций команды — типы, required, choices, min/max. Клиент строит
по ней форму ввода. См. Опции.
versionintegerВерсия команды. Увеличивается на 1 при фактическом изменении этой команды через
bulk-overwrite (PUT /api/bot/commands); идентичный повторный PUT версию не
меняет. Новой команде присваивается version: 1.
Вызов команды
POST /api/chat/{space_id}/{channel_id}/commands/invoke вызывает команду
выбранного бота. Аутентификация — Authorization: Bearer <user_token>.
bot_user_idUUIDrequiredID бота из каталога (bot_user_id).
commandstringrequiredИмя команды без /. 1..32 символа; нормализуется на сервере (strip() +
lower()).
optionsobjectЗначения опций как {option_name: value}. По умолчанию — пустой объект.
Валидируются по схеме команды (см. ниже).
curl -X POST https://api.reasonspace.ru/api/chat/{space_id}/{channel_id}/commands/invoke \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{
"bot_user_id": "e8e311f6-...",
"command": "weather",
"options": { "city": "Москва" }
}'
Значения options едут боту plaintext — в обход E2E-шифрования чата, по
явному opt-in пользователя (вызвать команду = осознанно отправить эти данные
конкретному боту). В чат, ленту и модерацию они не попадают.
Проверки доступа
Перед эмитом события платформа последовательно проверяет (порядок — как в коде):
Space существует и space.can_see_room(channel_id, actor) истинно. Иначе
— 403/404.
У бота есть membership в Space, channel_id входит в его
allowed_channel_ids, и выдан scope commands. Иначе — 403.
Состояние бота — active. Иначе — 404 (несуществующий/неактивный бот
маскируется как «не найден»).
Команда с таким именем зарегистрирована у бота и enabled. Иначе —
404.
validate_invocation приводит типы, отбрасывает неизвестные опции
(forward-compat) и проверяет схему. Иначе — 400.
Валидация значений (validate_invocation)
- Типы. Значения приводятся к типу опции:
string,integer→ int,number→ float,boolean→ bool,user/channel→ UUID-строка. Несоответствие →400. required. Отсутствие обязательной опции →400. Необязательные, если не переданы, просто пропускаются.choices. Если у опции задан список choices, значение должно быть одним из ихvalue— иначе400.min/max. Дляinteger/numberпроверяются границы — выход за пределы →400.- Размер ≤ 1024 байт. Суммарный объём типизированных значений (UTF-8)
ограничен 1024 байтами — анти-«/note <дамп чата>». Превышение →
400, событие боту не уходит.
Ответ
При успехе — 200 OK:
{
"ok": true,
"interaction_id": "7c1f..."
}
okbooleanВсегда true при успешном вызове.
interaction_idstringUUID инвокации. Тот же interaction_id придёт боту в событии
command.invoked и в эфемерном ответе — для корреляции на клиенте.
Ответ синхронный и не содержит результата команды. После валидации
платформа адресно эмитит боту webhook command.invoked; бот отвечает
эфемерно по interaction_token. См.
Событие и ответ.
Errors
| HTTP | Когда |
|---|---|
400 | Значения опций не прошли валидацию (типы / required / choices / min-max / > 1024 байт) |
403 | Актор не видит канал, либо бот недоступен для команд в этом канале (нет membership / канала в allowed_channel_ids / scope commands) |
404 | Бот не найден или неактивен, либо команда не существует / отключена |
429 | Превышен rate-limit инвокаций (см. ниже) |
Rate-limit
Инвокации лимитируются по паре (пользователь, бот) — анти-флуд вызовов конкретного бота одним юзером:
limit20 / 60 секундДо 20 вызовов за 60 секунд на каждую пару (actor_user_id, bot_user_id).
При превышении — 429 Too Many Requests с заголовком Retry-After: 60.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Лимит проверяется до проверок доступа и валидации. Каталог
(GET .../commands) этим лимитом инвокаций не покрыт (он защищён только
авторизацией и проверкой доступа к каналу).
Пример (Python SDK)
SDK reasonspace-bot покрывает bot-сторону (регистрация набора команд,
приём command.invoked, эфемерный ответ). Сама инвокация — действие
пользователя из клиента; ниже показано, как бот декларирует команду, которую
юзер затем вызовет:
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, а не в коде. См. Scopes.