Внешний обработчик

Внешний обработчик — это мост между вашим кодом и 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: "Кнопка нажата!" } }],
  }));
}

Что дальше