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

Инвокация (юзер)

Команду бота вызывает пользователь из канала — это делает клиент Reason Space, но контракт открыт и описан ниже. Платформа предоставляет два Bearer-эндпоинта: каталог доступных команд (для автокомплита) и сам вызов.

Это user-facing часть цепочки (Bearer-токен пользователя), не bot-API. Регистрация набора команд и приём события — на стороне бота: см. Регистрацию и Событие и ответ. Slash-команды — единственный inbound-канал «юзер → бот»: обычные сообщения юзеров боту как webhook не доставляются (чат E2E-encrypted).

Каталог команд для автокомплита

GET /api/chat/{space_id}/{channel_id}/commands возвращает список команд, доступных пользователю в этом канале. Клиент использует его для автокомплита при вводе /.

space_idUUIDrequired

Space, в котором находится канал.

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_idstring

ID бота, которому принадлежит команда. Передаётся обратно при вызове.

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_idUUIDrequired

ID бота из каталога (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_idstring

UUID инвокации. Тот же 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.

Дальше