Зовнішній обробник

Зовнішній обробник — це міст між вашим кодом і Telegram. Коли логіки конструктора не вистачає, ви віддаєте окрему реакцію своєму серверу: він отримує події і відповідає командами у форматі Telegram Bot API, а GetMyBot бере на себе транспорт, доставку і весь Telegram.

Що це

Реакції в GetMyBot збираються візуально: тригер → умови → дії. Складну або нестандартну логіку — власну базу, розрахунки, звернення до сторонніх систем, ML, розгалужені сценарії — у конструкторі виразити важко. Зовнішній обробник знімає цю стелю: ви підключаєте власний бекенд і віддаєте йому на обробку цілі реакції.

Зовнішній сервіс отримує від GetMyBot події (спрацював тригер, користувач відповів текстом, натиснув кнопку) і у відповідь командує боту, що надіслати. GetMyBot виконує команди своїм токеном бота — поверх працюють rate-limit, ретраї, дедублікація, журнал діалогу.

Модель роботи

  • GetMyBot — це WebSocket-сервер. Ваш сервіс сам підключається до нього і не потребує публічної адреси або вебхук-ендпоінту. Вихідних з'єднань до вашого сервісу GetMyBot не робить, тому SSRF-поверхні немає.
  • Автентифікація — токеном інтеграції. Сервіс пред'являє токен при підключенні; GetMyBot звіряє його з хешем і тримає з'єднання.
  • Події та команди — у форматі Telegram Bot API. Сервіс отримує подію, а відповідає масивом викликів виду «метод + параметри», точнісінько як при роботі зі звичайним ботом.
  • Команди виконує GetMyBot своїм токеном. Сервіс ніколи не звертається до Telegram напряму: команди проходять валідацію і потрапляють у внутрішній outbox GetMyBot, звідки надсилаються токеном бота з rate-limit, ретраями, дедублікацією та журналом.

Одне з'єднання обслуговує всі діалоги однієї інтеграції — події різних користувачів мультиплексуються і розрізняються за полем session_id.

Налаштування інтеграції

  1. Відкрийте розділ Інтеграції у меню ліворуч і натисніть Додати інтеграцію.
  2. Виберіть сервіс Зовнішній обробник і задайте назву.
  3. Після створення картка покаже:
    • Адреса WebSocket — вигляду wss://api.mybot.app/ext/ws/<integrationID>, де <integrationID> — UUID цієї інтеграції. За нею підключається ваш сервіс. Без id у шляху з'єднання не встановиться.
    • Токен підключення — показується один раз при створенні. Скопіюйте та збережіть його в надійному місці; переглянути токен повторно неможливо (він зберігається лише хешем).
    • Перевипуск токена — кнопка генерує новий токен і одразу знецінює старий. Після перевипуску оновіть токен у своєму сервісі.
    • Індикатор онлайн — показує, чи тримає ваш сервіс активне з'єднання просто зараз.

Додаткові параметри інтеграції, що впливають на протокол:

  • session_ttl_seconds — час життя сесії в секундах (за замовчуванням 3600).
  • on_unavailable_reaction_id — запасна реакція, яка запускається, якщо сервіс офлайн у момент спрацьовування (див. Надійність).

Прив'язка до реакції

Зовнішній обробник підключається не окремим тригером, а дією. У редакторі реакції додайте дію Передати зовнішньому обробнику і виберіть потрібну інтеграцію.

Відкрити діалог може будь-який наявний тригер GetMyBot — команда, текст, натискання кнопки, параметр із посилання, вхідний веб-запит, розклад. Коли такий тригер спрацьовує і доходить до дії, GetMyBot відкриває проксі-сесію і надсилає сервісу подію session.open.

Якщо в цей момент сервіс не підключений, GetMyBot запустить запасну реакцію з налаштування інтеграції (on_unavailable_reaction_id). Якщо запасна не задана — нічого не відбудеться (тихо, без помилки користувачу).

Підключення та авторизація

Ендпоінт: GET /ext/ws/{integrationID} поверх WebSocket (wss). integrationID — UUID інтеграції типу «Зовнішній обробник». Ліміт розміру кадру — 256 KiB.

Авторизуватися можна одним із двох способів.

1. Заголовок при рукостисканні (рекомендовано). Передайте токен у заголовку Authorization:

Authorization: Bearer <token>

2. Auth-кадром. Якщо Bearer-заголовок не передано, першим кадром протягом 10 секунд після встановлення з'єднання надішліть:

{ "type": "auth", "token": "<token>" }

Якщо за 10 секунд auth-кадр не надійшов — з'єднання закривається з кодом 4401. Невірний токен будь-яким зі способів — теж close 4401.

Токен генерується при створенні інтеграції, показується один раз і зберігається лише хешем (sha256), звірка — constant-time. Ротація токена:

POST /api/bots/{botID}/integrations/{integrationID}/rotate-token

Ендпоінт повертає новий plaintext-токен; старий одразу перестає працювати.

Події: бот → сервіс

Кожна подія — JSON-кадр (EventEnvelope). Поле type — одне з: session.open, message, callback, session.cancel, session.expired.

Повний набір полів конверту:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Іван",
    "username": "ivan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "сирий Telegram Update (raw JSON)" },
  "event": { "...": "нормалізована подія GetMyBot (raw JSON)" }
}
  • session_id — ідентифікатор сесії (діалогу).
  • bot_id — ідентифікатор бота.
  • user — дані користувача: tg_user_id (int64), опціональні first_name, username, а також params — параметри користувача (зокрема з посилання), map ключ→значення.
  • chat — чат: id (int64) і type (наприклад "private").
  • update — сирий Telegram Update (raw JSON). Присутній для session.open, message, callback.
  • event — нормалізована подія GetMyBot (raw JSON).

Наповнення за типами відрізняється:

  • session.open — спрацював тригер, відкрито новий діалог. Є update (сирий Update) і event (повна нормалізована подія), user.params заповнені.
  • message — надходить, коли сервіс «тримає очікування» (expect = text/any) і користувач надіслав повідомлення. Наповнення таке саме, як у session.open.
  • callback — користувач натиснув кнопку, яку раніше надіслав сервіс. update сирий; у user заповнений лише tg_user_id; event містить лише вихідний callback_data, заданий сервісом у кнопці:
{ "callback_data": "<оригінальне значення, задане сервісом у кнопці>" }
  • session.cancel / session.expired — мінімальний конверт: session_id, bot_id, user.tg_user_id, chat.id (для cancel ще chat.type). Полів update/event немає. Ці події доставляються лише якщо сервіс онлайн — в офлайн-чергу вони не кладуться.

Команди: сервіс → бот

Команда — JSON-кадр (CommandEnvelope). Поле type — одне з: execute, session.close, auth, ping, pong.

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "Привіт" } }
  ],
  "expect": "none"
}
  • session_id — сесія, до якої належить команда.
  • token — лише для type: "auth" (див. Підключення та авторизація).
  • methods — масив об'єктів { "method": "<ім'я методу Telegram Bot API>", "params": { ... } }. Ім'я методу — точнісінько як у Telegram Bot API (наприклад sendMessage), params — об'єкт параметрів цього методу.
  • expect — чи чекає сервіс відповіді користувача: "none", "text" або "any" (порожнє значення трактується як none).
  • session.close — явно закрити діалог.

Команди не йдуть до Telegram напряму: GetMyBot валідує їх і кладе у свій outbox, звідки надсилає токеном бота (з rate-limit, ретраями, дедублікацією, журналом).

Як обробляється execute

Команда execute дропається цілком, якщо: session_id порожній; сесія не знайдена або не в статусі open; сесія належить іншій інтеграції; сесія прострочена.

Далі:

  • Rate-limit: до 60 execute на хвилину на сесію (ковзне вікно). Понад ліміт — дроп.
  • До 30 методів в одному execute; зайві мовчки відкидаються.
  • На кожен метод перевіряється whitelist (див. Дозволені методи) — метод не зі списку відхиляється.
  • Chat-guard: якщо в params є chat_id або from_chat_id зі значенням, що не дорівнює 0 і не дорівнює chat_id сесії, — весь метод відхиляється (не можна слати в чужий чат). chat_id можна не вказувати взагалі: GetMyBot примусово проставить чат сесії.
  • Inline-кнопки з callback_data автоматично токенізуються (внутрішній вигляд x:<token>); час життя кнопки = до закінчення сесії. Кнопки url/webapp/switch_inline не чіпаються.

Дозволені методи

Сервіс командує ботом, тому набір методів обмежений whitelist — лише надсилання та робота з повідомленнями поточного діалогу. Методи не зі списку мовчки ігноруються (це не помилка).

  • надсилання: sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction;
  • правка та видалення: editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage;
  • відповідь на натискання: answerCallbackQuery;
  • пересилання та копіювання: forwardMessage, copyMessage;
  • закріплення: pinChatMessage, unpinChatMessage.

Акаунт-рівневі методи (setWebhook, getUpdates, logOut, close, setMyCommands тощо) не дозволені.

Очікування та життєвий цикл сесії

Після кожного execute доля сесії залежить від expect і від того, чи були в команді inline-кнопки з callback_data:

  • expect = text або any — наступне повідомлення користувача буде переслано сервісу подією message.
  • expect = none І в команді НЕ було inline-кнопок з callback — сесія автоматично закривається. Це «термінальне» повідомлення.
  • expect = none, АЛЕ є callback-кнопки — сесія залишається відкритою: натискання ловляться, поки живі токени кнопок (до TTL).

Важливо: expect не впливає на прийом натискань кнопок. Натискання токенізованих кнопок ловляться незалежно від значення expect — поки жива сесія і токен кнопки. expect керує лише тим, чи чекає сервіс текстової/довільної відповіді.

Межі діалогу

Контекст діалогу живе рівно до того часу, поки сесія відкрита:

  • Текстова відповідь при активному очікуванні (expect = text/any) — йде сервісу подією message, а не запускає звичайні реакції бота.
  • Натискання токенізованої кнопки — йде сервісу подією callback з оригінальним callback_data. GetMyBot завжди сам підтверджує натискання (answerCallbackQuery); перевіряється належність натискання (бот + користувач + чат сесії). Кнопкою сесія не закривається.
  • /cancel (а також /cancel@bot і «скасувати») при активній зовнішній сесії — сесія закривається, сервісу надсилається session.cancel, користувачу відправляється «Скасовано.».
  • Один діалог на користувача. Відкриття нової сесії витісняє попередню відкриту сесію цього користувача — їй надсилається session.cancel.
  • Закінчення TTL. За таймером сесія позначається expired і (якщо сервіс онлайн) сервісу надсилається session.expired.

Ліміти та таймаути

  • Розмір кадру: 256 KiB.
  • TTL сесії: за замовчуванням 3600 секунд (налаштовується параметром session_ttl_seconds інтеграції).
  • Частота команд: 60 execute на хвилину на сесію (ковзне вікно).
  • Методів в одному execute: до 30 (зайві відкидаються).
  • Відкритих сесій на інтеграцію: до 1000.
  • Офлайн-черга: до 100 подій на сесію.
  • Heartbeat: ping кожні 30 секунд; таймаут відсутності активності — 60 секунд.
  • Таймаут auth-кадру: 10 секунд.

Надійність (heartbeat, офлайн, черга, fallback)

  • Heartbeat. Сервер раз на 30 секунд надсилає кадр {"type":"ping"}. Якщо від клієнта немає активності довше 60 секунд — з'єднання закривається. Клієнт може надсилати свої ping/pong для підтримання активності. Розрив з'єднання не вбиває діалоги: сесії живуть у БД до TTL або перепідключення.
  • Перепідключення — на стороні сервісу. Якщо з'єднання обірвалося, ваш сервіс має підключитися заново. Відкриті сесії в БД переживають рестарти до закінчення TTL.
  • Fallback при офлайні на старті. Якщо в момент спрацьовування реакції сервіс не підключений — запускається запасна реакція з налаштування інтеграції (on_unavailable_reaction_id). Якщо вона не задана — нічого не відбувається (тихо).
  • Офлайн-черга. Події message/callback, не доставлені через офлайн, складаються в офлайн-чергу (до 100 подій, FIFO, зберігається до закінчення сесії) і досилаються при перепідключенні. Події session.open/session.cancel/session.expired у чергу не кладуться.
  • Гарантія доставки — at-most-once. При переповненні або протуханні черги події відкидаються. Проєктуйте логіку так, щоб пропуск окремої події не ламав сценарій.

Індикатор онлайн

Перевірити, чи тримає сервіс активне з'єднання, можна ендпоінтом:

GET /api/bots/{botID}/integrations/{integrationID}/status

Відповідь:

{ "online": true }

Той самий індикатор доступний на картці інтеграції в кабінеті. Аналог у MCP — інструмент get_integration_status.

Безпека

GetMyBot виконує команди стороннього сервісу своїм токеном, тому захист суворий.

  • Немає SSRF-поверхні. GetMyBot виступає WebSocket-сервером і сам вихідних з'єднань до сервісу не робить — це сервіс підключається до GetMyBot. Керованого ззовні URL, за яким платформа кудись ходить, немає.
  • Токен зберігається хешем (sha256), звірка constant-time, підтримана ротація.
  • Ізоляція орендаря. Команда жорстко прив'язана до своєї інтеграції; сесія — до бота + користувача + чату. Події чужих ботів у ваш сокет не потрапляють.
  • Whitelist методів + примусовий chat_id не дають перетворити бота на розсильник у довільні чати.
  • Токен бота назовні не передається — сервіс ніколи не спілкується з Telegram напряму.
  • Маскування секретів. Токен інтеграції та чутливі значення приховуються в журналі та логах.

Приклади

Мінімальний обробник: на відкриття діалогу надсилає повідомлення і закриває сесію (expect: "none"). Підставте id інтеграції та токен. Адреса обов'язково містить <integrationID> у шляху.

Node.js

import WebSocket from "ws";

const URL = `wss://api.mybot.app/ext/ws/${process.env.MYBOT_INTEGRATION_ID}`;
const ws = new WebSocket(URL, {
  headers: { Authorization: `Bearer ${process.env.MYBOT_TOKEN}` },
});

ws.on("message", (raw) => {
  const ev = JSON.parse(raw);
  if (ev.type === "session.open") {
    ws.send(JSON.stringify({
      type: "execute",
      session_id: ev.session_id,
      expect: "none",
      methods: [{ method: "sendMessage", params: { text: "Привіт від зовнішнього обробника!" } }],
    }));
  }
});

Python

import json, os, asyncio, websockets

async def main():
    url = f"wss://api.mybot.app/ext/ws/{os.environ['MYBOT_INTEGRATION_ID']}"
    headers = {"Authorization": f"Bearer {os.environ['MYBOT_TOKEN']}"}
    async with websockets.connect(url, additional_headers=headers) as ws:
        async for raw in ws:
            ev = json.loads(raw)
            if ev["type"] == "session.open":
                await ws.send(json.dumps({
                    "type": "execute",
                    "session_id": ev["session_id"],
                    "expect": "none",
                    "methods": [
                        {"method": "sendMessage", "params": {"text": "Привіт від зовнішнього обробника!"}}
                    ],
                }))

asyncio.run(main())

Inline-кнопка та обробка callback

Щоб зловити натискання, надішліть кнопку з callback_data (GetMyBot токенізує її автоматично). При expect: "none" з кнопкою сесія залишається відкритою, поки живий токен кнопки — натискання прийде подією callback, в якій event.callback_data дорівнює вихідному значенню. Підтверджувати натискання (answerCallbackQuery) не потрібно — GetMyBot робить це сам.

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "Натисніть кнопку",
        reply_markup: { inline_keyboard: [[{ text: "Поїхали", callback_data: "go" }]] },
      },
    }],
  }));
} else if (ev.type === "callback" && ev.event.callback_data === "go") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{ method: "sendMessage", params: { text: "Кнопку натиснуто!" } }],
  }));
}

Що далі