Guide: Bot Interactive Quickstart — от нуля до первой кнопки
Linear onboarding для интерактивных компонентов. Цель — поднять approval-flow "Approve/Reject" за 5-10 минут. За полным справочником полей, флагов и visibility — идите в reference/interactive-components.md.
Что делаем
- Бот отправляет сообщение в канал с кнопками Approve / Reject.
- В основном сценарии этого гайда PartyFlow шлёт HMAC-подписанный POST на callback URL бота, а бот отвечает sync JSON: обновляет сообщение или показывает ephemeral-текст.
- Если у вас нет публичного HTTPS endpoint'а, включите
event_delivery_mode="poll": клики придут черезGET /api/v1/bot/events, а ответ отправляется вPOST /api/v1/bot/interactions/{interaction_id}/respond.
В конце у вас будет рабочий bot + receiver, которые можно расширить под любой interactive flow.
Понадобятся: инстанс PartyFlow, bot token (fri_bot_...), Python 3.10+. Для callback mode нужен публичный HTTPS-endpoint (ngrok/cloudflared для локальной разработки подойдут); для poll mode публичный endpoint не нужен.
Шаг 1. Создать бота
Если бота ещё нет — guides/create-bot.md, шаги 1-3. Токен сохраните в secrets manager. Бот должен быть добавлен в канал, в который будет слать сообщение.
export BASE='https://api.partyflow.ru'
export TOKEN='fri_bot_...'
export CHANNEL_ID='<uuid-канала>'Шаг 2. Выбрать режим доставки
Callback mode
Используйте callback mode, если бот может принять публичный HTTPS POST.
Бот сообщает PartyFlow, куда слать click-события, через PATCH /api/v1/bot/interactive_config. В ответ PartyFlow выдаёт signing secret — показывается один раз, сохраните немедленно.
curl -X PATCH "$BASE/api/v1/bot/interactive_config" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"callback_url": "https://your-bot.example.com/partyflow/interactive"}'Ответ при первой настройке:
{
"ok": true,
"callback_url": "https://your-bot.example.com/partyflow/interactive",
"signing_secret": "<hex-secret>"
}export BUTTONS_SECRET='<hex-secret>'Заметки:
- Этот secret —
buttons_signing_secret, отличный от signing secret у обычных webhook'ов. - Если повторно вызвать
PATCH— вернётся{"ok": true, "callback_url": "..."}безsigning_secret. Используется прежний. - Для ротации:
POST /api/v1/bot/interactive_config/rotate_secret— выдаёт новый, все активные кнопки инвалидируются.
Проверить текущий статус без секрета:
curl "$BASE/api/v1/bot/interactive_config" -H "Authorization: Bearer $TOKEN"
# {"ok": true, "enabled": true, "callback_url": "https://..."}Poll mode
Используйте poll mode, если бот запускается в dev/CI/on-prem и не имеет публичного callback URL:
curl -X PATCH "$BASE/api/v1/bot/me" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"event_delivery_mode": "poll"}'В этом режиме пропустите HMAC receiver из шага 4. После клика читайте событие через:
curl "$BASE/api/v1/bot/events?cursor=&limit=100" \
-H "Authorization: Bearer $TOKEN"payload_json будет содержать canonical interactive payload плюс interaction_id; ответ отправляется отдельным запросом POST /api/v1/bot/interactions/{interaction_id}/respond. Полный контракт — reference/bot-event-poll.md.
Шаг 3. Отправить сообщение с кнопками
Используем POST /api/v1/bot/messages с полем metadata_json. signed_token для каждой кнопки PartyFlow проставит автоматически — боту не нужно их вычислять.
METADATA=$(cat <<'JSON'
{
"version": 1,
"blocks": [
{
"type": "actions",
"elements": [
{
"type": "button",
"action_id": "approve",
"text": "Approve",
"style": "primary",
"data": "{\"release\":\"v2.3.0\"}"
},
{
"type": "button",
"action_id": "reject",
"text": "Reject",
"style": "danger",
"data": "{\"release\":\"v2.3.0\"}"
}
]
}
]
}
JSON
)
curl -X POST "$BASE/api/v1/bot/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg cid "$CHANNEL_ID" --arg meta "$METADATA" '{
conversation_id: $cid,
content: "Release v2.3.0 готов к выкатке. Подтверди или отклони.",
metadata_json: $meta
}')"В канале появится сообщение с двумя кнопками. Если выбрали callback mode, не кликайте пока — callback receiver ещё не поднят. В poll mode можно кликать сразу и смотреть событие в GET /api/v1/bot/events.
Шаг 4. Поднять receiver
FastAPI-endpoint, который принимает click-события от PartyFlow. Три задачи:
- Проверить HMAC-подпись.
- Распарсить событие (
button.click,select.submit,modal.submit). - Вернуть sync JSON-ответ в пределах 3 секунд.
"""
PartyFlow interactive callback receiver.
"""
import hashlib
import hmac
import json
import os
import time
from fastapi import FastAPI, Header, HTTPException, Request
BUTTONS_SECRET = os.environ["BUTTONS_SECRET"].encode()
ALLOWED_USERS = set(os.environ.get("APPROVERS", "").split(",")) # optional ACL
app = FastAPI()
def verify_signature(secret: bytes, timestamp: str, body: bytes, signature: str) -> bool:
# Anti-replay: reject requests older than 5 minutes.
try:
if abs(int(time.time()) - int(timestamp)) > 300:
return False
except ValueError:
return False
base = f"v1:{timestamp}:".encode() + body
expected = "sha256=" + hmac.new(secret, base, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/partyflow/interactive")
async def interactive(
request: Request,
x_partyflow_signature: str = Header(default=""),
x_partyflow_timestamp: str = Header(default=""),
x_partyflow_event: str = Header(default=""),
):
body = await request.body()
if not verify_signature(BUTTONS_SECRET, x_partyflow_timestamp, body, x_partyflow_signature):
raise HTTPException(401, "invalid signature")
event = json.loads(body)
action_id = event.get("action_id")
actor_user_id = event.get("actor_user_id")
# button.click — approve/reject release.
if x_partyflow_event == "button.click":
# Optional: restrict by user list. If you set visibility_user_ids
# in the button schema, PartyFlow already enforces this — but a
# server-side check is still cheap insurance.
if ALLOWED_USERS and actor_user_id not in ALLOWED_USERS:
return {"ephemeral_text": "У вас нет прав для этого действия."}
data = json.loads(event.get("data") or "{}")
release = data.get("release", "?")
if action_id == "approve":
return {
"update_message": {
"text": f":white_check_mark: Release {release} approved by <@{actor_user_id}>. Deploying…",
"metadata_json": "", # пустая строка = убрать все интерактивные блоки
}
}
if action_id == "reject":
return {
"update_message": {
"text": f":x: Release {release} rejected by <@{actor_user_id}>.",
"metadata_json": "",
}
}
# Default — silent ack.
return {"noop": True}Запустить:
pip install fastapi uvicorn
export BUTTONS_SECRET='<тот-же-hex-secret-из-шага-2>'
uvicorn receiver:app --host 0.0.0.0 --port 8081Для локальной разработки публичный URL можно получить через ngrok http 8081. Его же нужно прописать в callback_url (шаг 2).
Poll mode вместо receiver
В poll mode бот получает тот же interactive payload внутри payload_json, но дополнительно с interaction_id:
{
"event_type": "button.click",
"payload_json": "{\"interaction_id\":\"int-1\",\"event_type\":\"button.click\",\"space_id\":\"sp-1\",\"actor_user_id\":\"u-1\",\"conversation_id\":\"conv-1\",\"message_id\":\"msg-1\",\"action_id\":\"approve\",\"data\":\"{\\\"release\\\":\\\"v2.3.0\\\"}\",\"occurred_at\":\"2026-04-18T10:00:00Z\"}"
}После JSON.parse(payload_json) отправьте ответ:
curl -X POST "$BASE/api/v1/bot/interactions/$INTERACTION_ID/respond" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"update_message":{"text":"Release approved.","metadata_json":""}}'Типы ответов
Callback receiver или poll response endpoint принимает одно из пяти взаимоисключающих тел:
| Ответ | Эффект |
|---|---|
{"update_message": {"text": "...", "metadata_json": "..."}} |
Обновить исходное сообщение. metadata_json: "" — убрать все блоки. |
{"send_message": {"text": "...", "metadata_json": "..."}} |
Новое сообщение в канал. |
{"ephemeral_text": "..."} |
Текст видит только нажавший, не сохраняется. |
{"open_modal": {...}} |
Открыть форму. Submit придёт как modal.submit: callback POST с X-PartyFlow-Event или poll event с interaction_id. |
{"noop": true} или пустое тело |
Ничего не показывать. |
В callback mode лимит sync-ответа — 3 секунды. Если нужен долгий процессинг — верните {"ephemeral_text": "Запущено, подожди минутку"} сразу, а результат пришлите обратным POST /api/v1/bot/messages позже. В poll mode interaction_id живёт 15 минут по умолчанию, но лучше отвечать быстро, пока пользователь помнит действие.
Шаг 5 (бонус). Modal submit
Если в ответ на клик вернули open_modal, пользователь увидит форму. Когда он submit'ит, PartyFlow шлёт отдельный запрос:
POST https://your-bot.example.com/partyflow/interactive
X-PartyFlow-Event: modal.submitВ poll mode modal.submit приходит через GET /api/v1/bot/events с interaction_id, как и button.click.
Body:
{
"event_type": "modal.submit",
"action_id": "deploy_config",
"actor_user_id": "...",
"space_id": "...",
"conversation_id": "...",
"message_id": "...",
"values": {"reason": "Fix critical bug", "notify": true},
"occurred_at": "..."
}Обработчик в receiver:
if x_partyflow_event == "modal.submit":
values = event.get("values", {})
reason = values.get("reason", "")
return {
"send_message": {
"text": f":rocket: Deploy started. Reason: {reason}",
"metadata_json": "",
}
}В callback mode open_modal в ответ на modal.submit не принимается: используйте send_message с новыми кнопками, если нужен многошаговый flow. В poll mode open_modal после modal.submit поддерживается через POST /api/v1/bot/interactions/{interaction_id}/respond; глубина цепочки ограничена настройкой инстанса.
Полная схема modal fields (text/textarea/select/checkbox) — reference/interactive-components.md → Modal submit flow.
Troubleshooting
| Симптом | Причина | Решение |
|---|---|---|
| В канале кнопки не появились | Не выбран режим доставки interactive-событий, либо ошибка в metadata_json. |
Для callback mode проверьте GET /api/v1/bot/interactive_config → "enabled": true. Для poll mode проверьте GET /api/v1/bot/me → "event_delivery_mode": "poll". Проверьте JSON через jq . и лимиты (blocks ≤ 5, elements ≤ 5, action_id уникален). |
400 invalid JSON при send |
metadata_json — не строка, а вложенный объект. |
Сериализуйте в строку: "metadata_json": "...", не "metadata_json": {...}. |
| Клик ничего не делает, receiver не вызывается | В callback mode callback_url недоступен из публичного интернета или неправильный URL в config. В poll mode receiver не вызывается по дизайну. |
Для callback mode используйте ngrok и проверьте GET /api/v1/bot/interactive_config. Для poll mode читайте GET /api/v1/bot/events и отвечайте через POST /api/v1/bot/interactions/{interaction_id}/respond. |
Poll возвращает events: [] после клика |
Бот не в event_delivery_mode="poll", используется не тот token, или вы передаёте cursor после нужного события. |
Проверьте GET /api/v1/bot/me, используйте тот же bot token, которым отправили сообщение, и временно запросите cursor=&limit=100. |
Receiver получает 401 от собственной проверки подписи |
Неправильный secret, clock skew, JSON парсится перед проверкой (меняет bytes). | Проверяйте подпись от raw await request.body() ДО json.loads. Проверьте, что BUTTONS_SECRET — это значение, полученное в шаге 2 (а не signing secret от обычных webhook'ов). Проверьте NTP (clock skew > 5 мин → отказ). |
410 Gone при клике |
Сообщение удалено, или signed_token кнопки устарел (default TTL 24ч), или секрет был ротирован. |
Ожидаемое поведение — старые кнопки инвалидируются после rotate_secret. Отправьте сообщение заново. |
429 Rate limited |
Пользователь жмёт кнопку слишком часто (30/мин per user × message) или бот в целом перегружен (600/мин). | Покажите ephemeral_text "Подождите немного". Проверьте, не запустили ли user'ы retry-storm. |
| Receiver ответил, но сообщение не обновилось | Receiver вернул 200, но JSON не распарсился, или таймаут > 3 сек. |
Проверьте, что Content-Type: application/json в ответе. Логируйте время ответа — если > 3 сек, выносите логику в фон и отвечайте быстрым ephemeral_text. |
| Кнопки видны не всем участникам | Вы указали visibility_user_ids в schema. |
Либо это намеренно (approval только для approver'ов), либо уберите это поле для "видно всем". |
Для отладки подписи callback mode локально — см. guides/verify-signatures.md (snippets на Python/Node/Go/Bash). Схема подписи у interactive та же, что у outgoing webhooks: v1:{timestamp}:{body}. В poll mode HMAC-подпись callback'а не используется, потому что события забираются Bearer-запросом.
Что дальше
- Select menus вместо кнопок — тот же
type: "actions"block, ноtype: "select"element сoptions[]. Событие —X-PartyFlow-Event: select.submit, в bodyselected_value. - Restricted actions — поле
visibility_user_idsв button/select element ограничивает, кто может кликать (1..100 user_ids). Enforce происходит на PartyFlow, receiver'у не нужно дублировать проверку. - Confirm dialogs — поле
confirm: {title, text, confirm_label, cancel_label}на button. Пользователь увидит модалку подтверждения до отправки click. - Full reference — reference/interactive-components.md для schema, styles, лимитов, error paths, best practices.
- Poll delivery — reference/bot-event-poll.md для
payload_json,interaction_idиnext_cursor.
Связанное
- reference/interactive-components.md — полный справочник bot-side.
- reference/bot-event-poll.md — доставка interactive-событий через Poll API.
- guides/verify-signatures.md — snippets для HMAC на разных языках.
- concepts/security.md — best practices для хранения
buttons_signing_secret.