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

Опции и лимиты

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.

ТипЗначение в инвокацииchoicesmin / maxПримечание
stringстрокаданетДолжно быть str, иначе ошибка
integerцелоедадаbool отвергается (строгий тип)
numberдробноедадаbool отвергается (строгий тип)
booleantrue / falseнетнетПринимается только настоящий bool
userUUID участниканетнетПриводится к строке UUID
channelUUID каналанетнетПриводится к строке 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.

descriptionstringrequired

1–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_BOT100Команд на одного бота (весь реестр)
MAX_OPTIONS_PER_COMMAND25Опций в одной команде
MAX_CHOICES25Элементов choices в одной опции
MAX_INVOCATION_PLAINTEXT_BYTES1024Суммарный размер значений одной инвокации

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

Отсутствие required-опции → missing required option: <name>.

Приведение типа

stringstr as-is; integerint (без bool); numberfloat (без bool); boolean → только bool; user/channel → строка UUID. Ошибка приведения → option <name>: bad <type> value.

choices

Если у опции есть choices — значение обязано быть в множестве value, иначе option <name>: value not in choices.

min / max

Для integer/number: typed < minbelow min; typed > maxabove 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/commandsbulk-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ГдеЧто значит
400PUT /commands, invokeCommandValidationError: невалидная схема или значения (тип, choices, min/max, размер, порядок required)
403invokeНет доступа к каналу, нет scope commands, или команда недоступна боту в этом канале
403respondinteraction_token невалиден/протух/принадлежит другому боту
404invokeБот не ACTIVE, команда не существует или disabled
429invokeRate limit (заголовок Retry-After в ответе)
429PUT /commandsRate limit (заголовок Retry-After не возвращается)