Внешний обработчик
Внешний обработчик — это мост между вашим кодом и 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.
Настройка интеграции
- Откройте раздел Интеграции в меню слева и нажмите Добавить интеграцию.
- Выберите сервис Внешний обработчик и задайте название.
- После создания карточка покажет:
- Адрес WebSocket — вида
wss://api.mybot.app/ext/ws/<integrationID>, где<integrationID>— UUID этой интеграции. По нему подключается ваш сервис. Без id в пути соединение не установится. - Токен подключения — показывается один раз при создании. Скопируйте и сохраните его в надёжном месте; повторно посмотреть токен нельзя (он хранится только хешем).
- Перевыпуск токена — кнопка генерирует новый токен и сразу обесценивает старый. После перевыпуска обновите токен в своём сервисе.
- Индикатор онлайн — показывает, держит ли ваш сервис активное соединение прямо сейчас.
- Адрес WebSocket — вида
Дополнительные параметры интеграции, влияющие на протокол:
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: "Кнопка нажата!" } }],
}));
}
Что дальше
- Интеграции — общий обзор подключаемых сервисов.
- Реакции — как собираются триггеры, условия и действия.
- Веб-запросы и вебхуки — более простой способ дёрнуть внешний URL без диалога.