Команды и кнопки
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": "Состояние сервисов"}]}'await bot.commands([
{ name: 'deploy', description: 'Выкатить ветку на стенд' },
{ name: 'status', description: 'Состояние сервисов' },
]);requests.put(f"{CALAB}/api/bots/me/commands", headers=H, json={"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:
{
"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"}]}]}}'const message = await bot.send(roomId, {
text: 'Проверьте черновик',
inlineKeyboard: {
allowedUserIds: [authorId],
rows: [{ buttons: [
{ id: 'confirm', label: 'Подтвердить', data: 'draft:42:v3' },
{ id: 'reject', label: 'Отклонить', data: 'draft:42:v3' },
] }],
},
});| Поле | Правило |
|---|---|
rows[].buttons[] | до 5 рядов по 5 кнопок |
id | уникален в клавиатуре, 1–64 символа: латиница, цифры, _, - |
label | 1–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 и через вебхук, если он настроен:
{
"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.