Опции и лимиты
Slash-команды — единственный inbound-канал «юзер → бот». Обычные сообщения юзеров боту не доставляются (нет webhook на чужой plaintext), поэтому аргументы команд едут plaintext в обход E2E только по явному opt-in юзера, и их объём строго ограничен схемой и суммарным размером.
Команды объявляются ботом глобально (PUT /api/bot/commands), а их видимость и
вызов в конкретном Space гейтятся scope commands в bot-membership. Scope
запрашивается при invite-в-Space, не в декораторе SDK.
Типы опций
OptionType — шесть типов. Каждый строго типизируется при вызове команды
(validate_invocation); неверный тип значения → ошибка 400.
| Тип | Значение в инвокации | choices | min / max | Примечание |
|---|---|---|---|---|
string | строка | да | нет | Должно быть str, иначе ошибка |
integer | целое | да | да | bool отвергается (строгий тип) |
number | дробное | да | да | bool отвергается (строгий тип) |
boolean | true / false | нет | нет | Принимается только настоящий bool |
user | UUID участника | нет | нет | Приводится к строке UUID |
channel | UUID канала | нет | нет | Приводится к строке UUID |
boolean строго типизирован: значение должно быть JSON-true/false. Для
integer/number значение true/false запрещено — _coerce явно
отбрасывает bool, чтобы true не «просочился» в число как 1.
Объявление опции
namestringrequired^[a-z0-9_-]{1,32}$ — нижний регистр, цифры, _, -; 1–32 символа.
Нормализуется (strip().lower()). Дубликаты внутри команды запрещены.
typeOptionTyperequiredОдин из: string, integer, number, boolean, user, channel.
Любое другое значение → ошибка bad option type.
descriptionstringrequired1–100 символов (после strip).
requiredbooleandefault: falseВсе required-опции обязаны идти до optional. Нарушение порядка → ошибка
required options must come before optional ones.
choicesarrayТолько для string / integer / number. До 25 элементов. Каждый элемент —
{ "name": str, "value": ... }; name обрезается до 100 символов. При вызове
значение опции должно совпадать с одним из value, иначе value not in choices.
minnumberТолько для integer / number. Проверяется при вызове (typed < min → ошибка).
maxnumberТолько для integer / number. Проверяется при вызове (typed > max → ошибка).
Лимиты
Точные значения из app/domain/bots/commands.py:
| Константа | Значение | Что ограничивает |
|---|---|---|
MAX_COMMANDS_PER_BOT | 100 | Команд на одного бота (весь реестр) |
MAX_OPTIONS_PER_COMMAND | 25 | Опций в одной команде |
MAX_CHOICES | 25 | Элементов choices в одной опции |
MAX_INVOCATION_PLAINTEXT_BYTES | 1024 | Суммарный размер значений одной инвокации |
MAX_INVOCATION_PLAINTEXT_BYTES считается как сумма UTF-8 байт всех типизированных
значений инвокации (len(str(typed).encode("utf-8"))). Превышение → ошибка
invocation exceeds 1024 plaintext bytes. Это анти-«/note <дамп чата>»:
структурная граница утечки plaintext в обход E2E.
Дополнительные лимиты декларации команды (validate_command):
- Имя команды:
^[a-z0-9_-]{1,32}$, без/(слэш добавляет UI). Дубликаты имён команд в одном реестре запрещены. - Описание команды: 1–100 символов.
- Описание опции: 1–100 символов.
Валидация при вызове
validate_invocation приводит значения к типам и проверяет схему. Неизвестные
опции (которых нет в схеме команды) молча отбрасываются (forward-compat).
Отсутствие required-опции → missing required option: <name>.
string → str as-is; integer → int (без bool); number → float
(без bool); boolean → только bool; user/channel → строка UUID.
Ошибка приведения → option <name>: bad <type> value.
Если у опции есть choices — значение обязано быть в множестве value,
иначе option <name>: value not in choices.
Для integer/number: typed < min → below min; typed > max → above max.
Сумма UTF-8 байт всех значений ≤ 1024, иначе invocation exceeds 1024 plaintext bytes.
Объявление команд (бот)
Реестр объявляется целиком (idempotent bulk-overwrite). Имена — без /.
curl -X PUT https://api.reasonspace.ru/api/bot/commands \
-H "Authorization: Bot bot_..." \
-H "Content-Type: application/json" \
-d '{
"commands": [
{
"name": "play",
"description": "Поставить трек в очередь",
"options": [
{ "name": "query", "type": "string", "description": "Название трека", "required": true },
{ "name": "volume", "type": "integer", "description": "Громкость 0–100", "min": 0, "max": 100 },
{
"name": "mode", "type": "string", "description": "Режим",
"choices": [
{ "name": "Обычный", "value": "normal" },
{ "name": "Повтор", "value": "loop" }
]
}
]
}
]
}'
@bot.command("play") # только имя; описание команды задаётся при register_commands
async def play(event): # event: CommandInvokedEvent
query = event.options["query"]
volume = event.options.get("volume", 50)
await bot.respond(event, f"Ставлю «{query}» (vol={volume})")
@bot.command(name=...) — без параметра scope=. Имя — без /. Право
на доставку command.invoked боту даёт scope commands, запрошенный при invite,
а не декоратор.
Управление реестром:
| Метод | Путь | Результат |
|---|---|---|
PUT | /api/bot/commands | bulk-overwrite реестра (idempotent) |
GET | /api/bot/commands | список своих команд |
DELETE | /api/bot/commands/{name} | удалить одну команду → 204 |
Каталог и вызов (юзер, Bearer)
curl https://api.reasonspace.ru/api/chat/{space_id}/{channel_id}/commands \
-H "Authorization: Bearer <user_jwt>"
curl -X POST https://api.reasonspace.ru/api/chat/{space_id}/{channel_id}/commands/invoke \
-H "Authorization: Bearer <user_jwt>" \
-H "Content-Type: application/json" \
-d '{
"bot_user_id": "e8e311f6-...",
"command": "play",
"options": { "query": "numb", "volume": 80, "mode": "loop" }
}'
bot_user_idUUIDrequiredБот, чью команду вызываем.
commandstringrequiredИмя команды (1–32 символа), нормализуется (strip().lower()), без /.
optionsobject{ имя_опции: значение }. Валидируется против схемы команды (см. выше).
Вызов проходит, только если у бота есть membership в Space, канал в
allowed_channel_ids, scope commands, бот в состоянии ACTIVE, а команда
существует и enabled. Иначе — 403 / 404.
Эфемерный ответ (бот)
Инвокация не пишется в чат, ленту или модерацию — она доставляется боту адресным
webhook-событием command.invoked со stateless interaction_token (действует в
течение TTL, см. ниже). Бот отвечает инвокеру эфемерно (targeted WS, не в общий чат):
curl -X POST https://api.reasonspace.ru/api/bot/interactions/respond \
-H "Authorization: Bot bot_..." \
-H "Content-Type: application/json" \
-d '{
"interaction_token": "<из события command.invoked>",
"content": "Добавил «numb» в очередь"
}'
Возвращает 204 No Content. content — 1–2000 символов.
interaction_token — stateless: HMAC-SHA256 над контекстом инвокации
(кто/где/какой бот), TTL 900 с (15 минут). Просроченный или чужой токен →
403 invalid or expired interaction. Хранилища нет — отвечайте сразу.
Коды ошибок
| HTTP | Где | Что значит |
|---|---|---|
400 | PUT /commands, invoke | CommandValidationError: невалидная схема или значения (тип, choices, min/max, размер, порядок required) |
403 | invoke | Нет доступа к каналу, нет scope commands, или команда недоступна боту в этом канале |
403 | respond | interaction_token невалиден/протух/принадлежит другому боту |
404 | invoke | Бот не ACTIVE, команда не существует или disabled |
429 | invoke | Rate limit (заголовок Retry-After в ответе) |
429 | PUT /commands | Rate limit (заголовок Retry-After не возвращается) |