PartyFlow

Reference: Interactive Components#

Аудитория этого документа — разработчики ботов.

Позволяет боту добавлять кнопки, select menus и модальные формы в сообщения. При клике PartyFlow доставляет interactive-событие либо синхронным HMAC POST на callback URL, либо через Bot Poll API, если у бота включён event_delivery_mode="poll".

Применение: approval flows, опросы, выбор окружения для деплоя, быстрые действия.


Base URL#

https://api.partyflow.ru

Во всех примерах — переменная $BASE.


Authentication#

Все interactive-запросы бота используют Bearer token того же бота, что и в Bot REST API:

Authorization: Bearer fri_bot_<token>

Для callback mode отдельно от signing_secret у бота есть buttons_signing_secret — подпись исходящих click-запросов. Генерируется при первой настройке callback URL и показывается один раз. В poll mode внешний секрет для кликов не нужен: бот получает события через авторизованный GET /api/v1/bot/events.


Setup flow#

Есть два режима доставки:

  1. Callback mode: бот вызывает PATCH /api/v1/bot/interactive_config с callback_url, получает signing_secret, затем PartyFlow шлёт HMAC POST с event button.click / select.submit / modal.submit.
  2. Poll mode: бот включает event_delivery_mode="poll", отправляет сообщения с metadata_json, а клики забирает через GET /api/v1/bot/events. В payload будет interaction_id; ответ отправляется через POST /api/v1/bot/interactions/{interaction_id}/respond.

В обоих режимах бот может вернуть update_message / send_message с новым metadata_json, чтобы обновить кнопки или показать следующий шаг.


Config endpoints#

PATCH /api/v1/bot/interactive_config#

Request:

{"callback_url": "https://your-bot.example.com/partyflow/interactive"}

Response при первой настройке (200):

{
  "ok": true,
  "callback_url": "https://your-bot.example.com/partyflow/interactive",
  "signing_secret": "dead...beef"
}

signing_secret возвращается один раз. Для ротации — POST /api/v1/bot/interactive_config/rotate_secret.

Response при повторной настройке URL (200):

{"ok": true, "callback_url": "https://..."}

signing_secret НЕ возвращается — используется тот же.

GET /api/v1/bot/interactive_config#

Статус interactive config (без секрета):

{"ok": true, "enabled": true, "callback_url": "https://..."}

POST /api/v1/bot/interactive_config/rotate_secret#

Генерирует новый signing_secret, возвращает plaintext один раз. Все активные кнопки инвалидируются (их signed_token больше не проходит verify).

DELETE /api/v1/bot/interactive_config#

Отключает interactive для бота. Все активные кнопки становятся неработающими.


Откуда берутся сообщения с кнопками#

Разместить интерактивные блоки в канале можно тремя путями:

  1. Через POST /api/v1/bot/messages с metadata_json — бот сам отправляет сообщение с блоками. Подходит для approval flows, interactive-опросов, deploy-команд. Пример ниже.
  2. Через incoming webhook — внешний сервис (CI, alerting, approval-система) шлёт сообщение в канал через POST /api/v1/webhooks/incoming/{token}, payload включает блоки. PartyFlow подпишет action tokens и доставит сообщение.
  3. В ответ на button.click / modal.submit — в response-пакете бот возвращает update_message / send_message с новым metadata_json, содержащим блоки. Типично для follow-up: после первого клика показать форму или обновлённое состояние. См. раздел Response (bot → PartyFlow).

Пример: отправить сообщение с кнопками через REST#

Быстрый рабочий пример для approval flow: две кнопки «Approve» и «Reject» в канале.

BASE='https://api.partyflow.ru'
TOKEN='fri_bot_...'
CHANNEL_ID='<uuid>'
 
# metadata_json — строка с JSON-схемой блоков (сериализуется отдельно от
# внешнего payload). signed_token для каждого action_id 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 готов к выкатке в prod. Подтверди или отклони.",
    metadata_json: $meta
  }')"

Ожидаемый ответ:

{"ok": true, "message_id": "<uuid>"}

Важно:

  • metadata_jsonстрока с JSON внутри, не вложенный объект. Сериализуется отдельно от основного payload (см. jq -n --arg meta … выше).
  • action_id должны быть уникальны в рамках сообщения.
  • data — opaque для PartyFlow строка; туда можно положить ID релиза, окружение, что угодно — она придёт обратно в click payload без изменений.
  • Перед первой отправкой сообщения с кнопками нужен один из режимов доставки: callback_url через PATCH /api/v1/bot/interactive_config или event_delivery_mode="poll" у бота.

Пошаговое onboarding — см. guides/bot-interactive-quickstart.md.

Полный список лимитов полей, styles и опций — ниже в разделе Schema.


Schema (bot_metadata_json)#

Schema, которой бот описывает интерактивные блоки (в incoming webhook payload или в response update_message / send_message):

{
  "version": 1,
  "blocks": [
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "action_id": "approve",
          "text": "Approve",
          "style": "primary",
          "data": "{\"env\":\"prod\"}",
          "visibility_user_ids": ["u-1", "u-2"],
          "confirm": {
            "title": "Подтвердите",
            "text": "Deploy to prod?",
            "confirm_label": "Deploy",
            "cancel_label": "Cancel"
          }
        },
        {
          "type": "select",
          "action_id": "pick_env",
          "placeholder": "Environment",
          "options": [
            {"value": "prod", "label": "Production"},
            {"value": "staging", "label": "Staging"}
          ]
        }
      ]
    }
  ]
}

Лимиты#

Поле Ограничение
blocks до 5
elements per block до 5 (итого 25 on message)
action_id ^[a-z0-9_-]{1,64}$, уникальный в рамках сообщения
text (label) до 75 символов
data до 2000 символов
placeholder до 100 символов
Select options до 25 опций
Option value до 150 символов, уникальный
Option label до 75 символов
confirm.title до 75
confirm.text до 300
visibility_user_ids до 100 user_ids
Весь bot_metadata_json до 32 KiB

Styles#

  • primary — основная (синяя)
  • danger — деструктивная (красная)
  • default / unset — обычная (нейтральная)

Signed token#

PartyFlow автоматически подписывает каждый action_id на send/edit — добавляет поле signed_token в JSON. В текущем контракте token связан с (buttons_signing_secret, action_id, data, exp) и проверяется при клике; default TTL — 24 часа (конфигурируется оператором). message_id не входит в HMAC-binding, поэтому bots должны обрабатывать клики идемпотентно.

Не нужно генерировать signed_token на стороне бота — PartyFlow делает это автоматически.


Click payload (PartyFlow → bot)#

В callback mode PartyFlow отправляет HTTP POST:

POST https://your-bot.example.com/partyflow/interactive
Content-Type: application/json
X-PartyFlow-Signature: sha256=<hex>
X-PartyFlow-Timestamp: 1713434400
X-PartyFlow-Delivery-Id: 8b4c7f2a-...
X-PartyFlow-Event: button.click   // или select.submit / modal.submit
X-PartyFlow-Webhook-Version: 1

Interactive payload использует canonical flat-поля event_type, actor_user_id, occurred_at. Старые имена type, user_id, timestamp не отправляются.

Body (button.click):

{
  "event_type": "button.click",
  "action_id": "approve",
  "data": "{\"env\":\"prod\"}",
  "actor_user_id": "...",
  "space_id": "...",
  "conversation_id": "...",
  "message_id": "...",
  "thread_id": "...",
  "occurred_at": "2026-04-18T..."
}

В poll mode этот же JSON лежит внутри payload_json события event_type="button.click" / select.submit, плюс добавляется interaction_id:

{
  "interaction_id": "uuid",
  "event_type": "button.click",
  "action_id": "approve",
  "data": "{\"env\":\"prod\"}",
  "actor_user_id": "...",
  "space_id": "...",
  "conversation_id": "...",
  "message_id": "...",
  "occurred_at": "..."
}

Body (select.submit):

{
  "event_type": "select.submit",
  "action_id": "pick_env",
  "data": "",
  "actor_user_id": "...",
  "space_id": "...",
  "conversation_id": "...",
  "message_id": "...",
  "selected_value": "prod",
  "occurred_at": "..."
}

Payload fields#

Поле button.click select.submit modal.submit Источник / описание
event_type да да да Тип события: button.click, select.submit, modal.submit. Дублирует X-PartyFlow-Event, чтобы body можно было обрабатывать без headers.
space_id да да да Server-authoritative space, которому принадлежит исходное сообщение.
actor_user_id да да да Пользователь, который кликнул кнопку/select или отправил modal.
conversation_id да да да Беседа исходного bot-сообщения.
message_id да да да Исходное bot-сообщение с interactive block.
action_id да да да ID action из metadata/schema. Для modal это action исходного клика, который открыл форму.
data да да нет Opaque строка из button/select element. Может быть пустой строкой.
selected_value нет да нет Выбранное значение select menu.
values нет нет да JSON object со значениями modal fields. Если форма пустая, приходит {}.
thread_id опционально опционально опционально Parent/root message ID, если исходное сообщение было в thread/reply context.
occurred_at да да да UTC timestamp в RFC3339/RFC3339Nano.
interaction_id poll only poll only poll only Есть только внутри payload_json в poll mode; нужен для POST /api/v1/bot/interactions/{interaction_id}/respond.

Response (bot → PartyFlow)#

В callback mode бот отвечает sync JSON в течение 3 секунд. В poll mode бот отправляет тот же JSON в:

POST /api/v1/bot/interactions/{interaction_id}/respond

Поддерживаются 5 типов ответа (взаимоисключающие):

1. update_message — обновить текущее сообщение#

{
  "update_message": {
    "text": "Approved by @user",
    "metadata_json": "{\"version\":1,\"blocks\":[...]}"
  }
}

Используется для approval flow: после клика убрать кнопки и показать итог.

2. send_message — новое сообщение в канал#

{
  "send_message": {
    "text": "Deploy started at 10:45",
    "metadata_json": "..."
  }
}

3. ephemeral_text — только клиенту#

{"ephemeral_text": "У вас нет прав для этого действия."}

Видит только нажавший — не сохраняется в канале.

4. open_modal — открыть форму#

{
  "open_modal": {
    "title": "Deploy config",
    "fields": [
      {"id": "reason", "type": "text", "label": "Reason", "required": true},
      {"id": "notify", "type": "checkbox", "label": "Notify channel"}
    ],
    "submit_label": "Deploy"
  }
}

Bot владеет schema (PartyFlow её не валидирует — просто передаёт клиенту).

5. noop — пустое тело или {"noop":true}#

Bot уже сделал работу и ничего показывать не нужно.


  1. Bot получает button.click → возвращает open_modal.
  2. PartyFlow генерирует single-use modal_token (TTL 15 мин) и отдаёт клиенту.
  3. Client показывает модалку, пользователь заполняет, submit.
  4. PartyFlow шлёт на callback URL event modal.submit:
{
  "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": "..."
}
  1. Bot отвечает update_message / send_message / ephemeral_text / noop. В poll mode также поддерживается open_modal после modal.submit, чтобы строить цепочки модалок; глубина ограничена настройкой инстанса.

HMAC verification#

Схема: v1:{timestamp}:{raw_body} → HMAC-SHA256 → hex → X-PartyFlow-Signature: sha256=<hex>.

Идентично outgoing webhooks и slash commands. Ключ — buttons_signing_secret (не signing_secret от webhooks).

Python example:

import hmac, hashlib, time
 
def verify_interactive(secret, timestamp, body, signature):
    if abs(int(time.time()) - int(timestamp)) > 300:
        return False
    base = f"v1:{timestamp}:{body.decode()}".encode()
    expected = "sha256=" + hmac.new(secret.encode(), base, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Rate limits#

Уровень Лимит
per-user × message 30 кликов/мин (anti double-click spam)
per-bot global 600 кликов/мин (anti stampede на callback endpoint)

Плюс idempotency guard: повторный клик одной и той же кнопки тем же пользователем в течение 60 сек возвращает 200 OK без повторного POST на бота.


Visibility / per-user access#

visibility_user_ids ограничивает кто может кликать кнопку. Список из 1..100 user_ids. Пустой = все участники канала. Проверка выполняется на стороне PartyFlow: user не в списке → 403.

Использование: approval от назначенного approver'а, restricted actions по ролям.


Error paths#

Ошибка HTTP Сценарий
400 Invalid JSON / schema violation
401 Invalid/missing session token
403 Cross-bot edit / visibility denied
410 Gone Message deleted / action_id больше не рендерится / modal_token expired
429 Rate limit tripped
503 Callback URL недоступен / rate-limiter circuit breaker

Best practices#

  1. Отвечай быстро. 3 секунды — жёсткий лимит.
  2. Idempotency. Bot должен сам быть idempotent — один и тот же action_id + actor_user_id может прилететь дважды при network retry.
  3. Храни buttons_signing_secret в secret manager.
  4. Не читай message_id / conversation_id из body — они уже server-verified.
  5. Не сохраняй data для долгих сценариев (token TTL 24h).
  6. Используй visibility_user_ids для approval flows с назначенным approver'ом.
  7. noop для silent ack когда action уже выполнен другим путём.