Перейти к содержимому
CalabДокументация

Команды и кнопки

Slash-команды бота — регистрация, адресация, разбор; кнопки под сообщениями — клавиатура, нажатия, идемпотентность.

Slash-команды

Команда — это обычное сообщение, которое начинается с /имя. Люди видят его как текст, а бот, которому оно адресовано, получает разобранную команду в поле message.command.

Регистрация

Бот публикует список своих команд — композер подсказывает их после /, люди видят их в профиле бота.

curl -s -X PUT $CALAB/api/bots/me/commands \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"commands": [{"name": "deploy", "description": "Выкатить ветку на стенд"},
                    {"name": "status", "description": "Состояние сервисов"}]}'
  • Запрос полностью заменяет список. Пустой список — убрать все команды.
  • До 100 команд; имя — [a-z0-9_]{1,32} (ведущий / отбрасывается), описание — до 256 символов, имена уникальны.
  • Команды всех ботов комнаты: GET /api/rooms/{id}/bot-commands — так композер строит подсказки.

Адресация

СообщениеКому команда
/name@username аргументыботу с этим username, если он видит комнату (регистрировать команду не обязательно)
/name аргументы в комнатеединственному боту комнаты, который зарегистрировал name; если таких несколько — это не команда, пишите @username
/name аргументы в DM с ботомэтому боту

После имени должен идти пробел или конец текста. Сообщения, написанные ботами, никогда не бывают командами — так боты не зацикливаются друг на друге.

Что получает бот

В событии messageCreate (gateway и вебхук) у адресованного бота заполнено message.command:

JSON
{
  "messageCreate": {
    "workspaceId": "0191f0c2-0000-7000-8000-00000000a001",
    "message": {
      "id": "0192a1d0-…",
      "roomId": "0191f0d4-0000-7000-8000-00000000b001",
      "authorId": "0191f0c2-7a10-7d3e-9b11-5c2f8e4a6d01",
      "content": "/deploy@opsbot feature/login",
      "command": { "botUserId": "0192a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b", "name": "deploy", "args": "feature/login" }
    }
  }
}

name — в нижнем регистре, args — остаток текста без пробелов по краям. Поле command есть только в событиях: в ответах REST и в истории его нет.

bot.on('command', async (c) => {
  if (c.name === 'roll') await bot.reply(c, String(1 + Math.floor(Math.random() * Number(c.args || 6))));
});

Упоминания

Упомянуть бота — обычное упоминание @<id бота> в тексте. GET /api/me/mentions возвращает сообщения, где упомянут бот.

Кнопки под сообщениями

Бот может прикрепить к своему сообщению клавиатуру — до 5 рядов по 5 кнопок. Человек нажимает кнопку, бот получает событие botCallback и решает, что делать.

Клавиатура

curl -s -X POST $CALAB/api/rooms/$ROOM/messages \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"content": "Проверьте черновик", "nonce": "draft-42-v3",
       "inlineKeyboard": {"allowedUserIds": ["0191f0c2-7a10-7d3e-9b11-5c2f8e4a6d01"],
         "rows": [{"buttons": [{"id": "confirm", "label": "Подтвердить", "data": "draft:42:v3"},
                               {"id": "reject", "label": "Отклонить", "data": "draft:42:v3"}]}]}}'
ПолеПравило
rows[].buttons[]до 5 рядов по 5 кнопок
idуникален в клавиатуре, 1–64 символа: латиница, цифры, _, -
label1–80 символов без управляющих
dataдо 512 байт UTF-8 — непрозрачные данные для бота
disabledкнопку нельзя нажать
allowedUserIdsдо 50 id; пусто — нажать может любой читатель с SEND_MESSAGES

Кнопки вызывают только бота-автора. У пересланных копий сообщения кнопки неактивны.

Изменение клавиатуры

PATCH /api/messages/{id}:

  • inlineKeyboard не передан — клавиатура остаётся; {"inlineKeyboard": {}} или пустые rows — убрать.
  • Чтобы поменять только клавиатуру, передайте "preserveContent": true без content. Флаг требует клавиатуру в запросе и не принимает непустой текст.
  • Без флага content работает как раньше: не передан или пуст — пустой текст (допустимо только с вложениями).
  • Любая правка текста или клавиатуры меняет серверный keyboardRevision.
await bot.edit(message.id, { inlineKeyboard: { rows: [] } });          // убрать, текст оставить
await bot.edit(message.id, { text: 'Черновик v4', inlineKeyboard: next }); // текст и клавиатура разом

SDK сам ставит preserveContent, когда в правке нет text.

Нажатие

Человек нажимает кнопку — клиент вызывает POST /api/messages/{id}/interactions с {buttonId, keyboardRevision, nonce}. Этот маршрут только для людей: бот нажимать кнопки не может. Сервер проверяет VIEW_ROOM + SEND_MESSAGES, актуальность сообщения и клавиатуры, список allowedUserIds, что бот-автор активен и доступен, блокировки и очищенную историю DM. Устаревшая клавиатура или недоступная кнопка — 409 CONFLICT с reason KEYBOARD_STALE / BUTTON_UNAVAILABLE.

Ответ 200 {interactionId} означает «принято», а не «выполнено». Сообщение в чат не публикуется. Интерфейс блокирует всю клавиатуру после приёма, пока бот не пришлёт новую ревизию.

Событие botCallback

Приходит только боту-автору — через его gateway и через вебхук, если он настроен:

JSON
{
  "botCallback": {
    "id": "0192a1e0-…",
    "botUserId": "0192a1b2-3c4d-7e5f-8a9b-0c1d2e3f4a5b",
    "userId": "0191f0c2-7a10-7d3e-9b11-5c2f8e4a6d01",
    "workspaceId": "0191f0c2-0000-7000-8000-00000000a001",
    "roomId": "0191f0d4-0000-7000-8000-00000000b001",
    "messageId": "0192a1d0-…",
    "buttonId": "confirm",
    "data": "draft:42:v3",
    "keyboardRevision": "2",
    "createdAt": "2026-10-14T10:05:00Z"
  }
}

workspaceId пуст в DM. userId и data берёт сервер — клиент их подделать не может.

bot.on('callback', async (c) => {
  if (await seen(c.id)) return;            // надёжная дедупликация по c.id
  await remember(c.id);
  const draft = await loadDraft(c.data);  // проверьте текущую версию черновика и автора
  if (c.userId !== draft.authorId || draft.version !== 'v3') return;
  await applyOnce(draft, c.buttonId);      // внешний эффект — идемпотентно
});

Гарантии доставки

  • Вебхук надёжен: доставка ставится в очередь атомарно с приёмом нажатия и повторяется по обычным правилам вебхуков. Важные действия стройте на вебхуке.
  • Gateway — «best effort» после фиксации: обычный RESUME перешлёт буферизованные события, но новая сессия пропущенные нажатия не вернёт.
  • Повтор нажатия тем же человеком с тем же nonce (1–64 байта) возвращает исходный interactionId без повторной доставки — даже после правки или удаления клавиатуры. Тот же nonce для другого нажатия — конфликт. Квитанции живут, пока сообщение не удалено физически. Разные nonce могут быть приняты оба.
  • Бот обязан надёжно дедуплицировать callback.id между транспортами и повторами и делать внешние эффекты идемпотентными. Нажатие, принятое до изменения черновика, не отменяется — сверяйте свою текущую версию. Кеш вебхуков в SDK ограничен и не даёт гарантии «ровно один раз».
  • Бот, убранный из последнего общего пространства, не получает старые нажатия из DM.