Zewnętrzny handler

Zewnętrzny handler to most między Twoim kodem a Telegram. Gdy logika kreatora jest niewystarczająca, oddajesz obsługę konkretnej reakcji własnym serwerem: otrzymuje on zdarzenia i odpowiada komendami w formacie Telegram Bot API, a GetMyBot zajmuje się transportem, dostarczaniem i całą warstwą Telegram.

Czym jest

Reakcje w GetMyBot budowane są wizualnie: wyzwalacz → warunki → akcje. Złożoną lub niestandardową logikę — własną bazę danych, obliczenia, integracje z zewnętrznymi systemami, ML, rozbudowane scenariusze — trudno wyrazić w kreatorze. Zewnętrzny handler usuwa ten sufit: podłączasz własny backend i oddajesz mu całe reakcje do obsługi.

Zewnętrzna usługa otrzymuje od GetMyBot zdarzenia (wyzwolił się wyzwalacz, użytkownik odpowiedział tekstem, nacisnął przycisk) i w odpowiedzi wydaje botowi polecenia, co wysłać. GetMyBot wykonuje komendy własnym tokenem bota — nad tym działają rate-limit, ponowienia, deduplikacja i dziennik dialogu.

Model działania

  • GetMyBot jest serwerem WebSocket. Twoja usługa sama łączy się z nim i nie potrzebuje publicznego adresu ani endpointu webhook. GetMyBot nie nawiązuje połączeń wychodzących do Twojej usługi, więc nie ma powierzchni SSRF.
  • Uwierzytelnianie tokenem integracji. Usługa przedstawia token przy połączeniu; GetMyBot weryfikuje go względem hasha i utrzymuje połączenie.
  • Zdarzenia i komendy w formacie Telegram Bot API. Usługa otrzymuje zdarzenie, a odpowiada tablicą wywołań w formacie „metoda + parametry", dokładnie tak jak przy pracy ze zwykłym botem.
  • Komendy wykonuje GetMyBot własnym tokenem. Usługa nigdy nie komunikuje się bezpośrednio z Telegram: komendy przechodzą walidację i trafiają do wewnętrznego outbox GetMyBot, skąd są wysyłane tokenem bota z rate-limit, ponowieniami, deduplikacją i dziennikiem.

Jedno połączenie obsługuje wszystkie dialogi jednej integracji — zdarzenia różnych użytkowników są multipleksowane i rozróżniane po polu session_id.

Konfiguracja integracji

  1. Otwórz sekcję Integracje w menu po lewej i kliknij Dodaj integrację.
  2. Wybierz usługę Zewnętrzny handler i podaj nazwę.
  3. Po utworzeniu karta pokaże:
    • Adres WebSocket — w formacie wss://api.mybot.app/ext/ws/<integrationID>, gdzie <integrationID> to UUID tej integracji. Pod tym adresem łączy się Twoja usługa. Bez ID w ścieżce połączenie nie zostanie nawiązane.
    • Token połączenia — pokazywany jeden raz przy tworzeniu. Skopiuj go i przechowaj w bezpiecznym miejscu; ponowne wyświetlenie tokenu nie jest możliwe (przechowywany jest wyłącznie jako hash).
    • Rotacja tokenu — przycisk generuje nowy token i natychmiast unieważnia stary. Po rotacji zaktualizuj token w swojej usłudze.
    • Wskaźnik online — pokazuje, czy Twoja usługa utrzymuje aktywne połączenie w tej chwili.

Dodatkowe parametry integracji wpływające na protokół:

  • session_ttl_seconds — czas życia sesji w sekundach (domyślnie 3600).
  • on_unavailable_reaction_id — reakcja zapasowa uruchamiana, gdy usługa jest offline w momencie wyzwolenia (zob. Niezawodność).

Powiązanie z reakcją

Zewnętrzny handler podłączany jest nie jako osobny wyzwalacz, lecz jako akcja. W edytorze reakcji dodaj akcję Przekaż do zewnętrznego handlera i wybierz odpowiednią integrację.

Dialog może otworzyć dowolny istniejący wyzwalacz GetMyBot — komenda, tekst, kliknięcie przycisku, parametr z linku, przychodzące żądanie web, harmonogram. Gdy taki wyzwalacz zostaje uruchomiony i dotrze do akcji, GetMyBot otwiera sesję proxy i wysyła usłudze zdarzenie session.open.

Jeśli w tym momencie usługa nie jest podłączona, GetMyBot uruchomi reakcję zapasową z ustawień integracji (on_unavailable_reaction_id). Jeśli reakcja zapasowa nie jest skonfigurowana — nic się nie wydarzy (bez błędu dla użytkownika).

Połączenie i autoryzacja

Endpoint: GET /ext/ws/{integrationID} przez WebSocket (wss). integrationID — UUID integracji typu „Zewnętrzny handler". Limit rozmiaru ramki — 256 KiB.

Autoryzacja możliwa jest na dwa sposoby.

1. Nagłówek przy uzgadnianiu (preferowane). Przekaż token w nagłówku Authorization:

Authorization: Bearer <token>

2. Ramka auth. Jeśli nagłówek Bearer nie został przesłany, w ciągu 10 sekund od nawiązania połączenia wyślij jako pierwszą ramkę:

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

Jeśli ramka auth nie nadejdzie w ciągu 10 sekund — połączenie zostanie zamknięte kodem 4401. Nieprawidłowy token przy obu metodach — również close 4401.

Token generowany jest przy tworzeniu integracji, pokazywany jeden raz i przechowywany wyłącznie jako hash (sha256), weryfikacja w czasie stałym (constant-time). Rotacja tokenu:

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

Endpoint zwraca nowy token w postaci plaintext; stary natychmiast przestaje działać.

Zdarzenia: bot → usługa

Każde zdarzenie to ramka JSON (EventEnvelope). Pole type — jedno z: session.open, message, callback, session.cancel, session.expired.

Pełny zestaw pól koperty:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Jan",
    "username": "jan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "surowy Telegram Update (raw JSON)" },
  "event": { "...": "znormalizowane zdarzenie GetMyBot (raw JSON)" }
}
  • session_id — identyfikator sesji (dialogu).
  • bot_id — identyfikator bota.
  • user — dane użytkownika: tg_user_id (int64), opcjonalne first_name, username, a także params — parametry użytkownika (w tym z linku), mapa klucz→wartość.
  • chat — czat: id (int64) i type (np. "private").
  • update — surowy Telegram Update (raw JSON). Obecny dla session.open, message, callback.
  • event — znormalizowane zdarzenie GetMyBot (raw JSON).

Zawartość różni się w zależności od typu:

  • session.open — wyzwalacz zadziałał, otwarty nowy dialog. Zawiera update (surowy Update) i event (pełne znormalizowane zdarzenie), user.params są wypełnione.
  • message — wysyłane, gdy usługa „utrzymuje oczekiwanie" (expect = text/any) i użytkownik przesłał wiadomość. Zawartość taka sama jak w session.open.
  • callback — użytkownik nacisnął przycisk wcześniej wysłany przez usługę. update surowy; w user wypełniony tylko tg_user_id; event zawiera wyłącznie oryginalne callback_data podane przez usługę w przycisku:
{ "callback_data": "<oryginalna wartość podana przez usługę w przycisku>" }
  • session.cancel / session.expired — minimalna koperta: session_id, bot_id, user.tg_user_id, chat.id (dla cancel również chat.type). Pola update/event nie są obecne. Zdarzenia te dostarczane są tylko jeśli usługa jest online — nie są zapisywane do kolejki offline.

Komendy: usługa → bot

Komenda to ramka JSON (CommandEnvelope). Pole type — jedno z: execute, session.close, auth, ping, pong.

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "Cześć" } }
  ],
  "expect": "none"
}
  • session_id — sesja, do której odnosi się komenda.
  • token — tylko dla type: "auth" (zob. Połączenie i autoryzacja).
  • methods — tablica obiektów { "method": "<nazwa metody Telegram Bot API>", "params": { ... } }. Nazwa metody — dokładnie tak jak w Telegram Bot API (np. sendMessage), params — obiekt parametrów tej metody.
  • expect — czy usługa oczekuje odpowiedzi użytkownika: "none", "text" lub "any" (pusta wartość traktowana jest jako none).
  • session.close — jawnie zamknij dialog.

Komendy nie trafiają bezpośrednio do Telegram: GetMyBot waliduje je i umieszcza w swoim outbox, skąd wysyła tokenem bota (z rate-limit, ponowieniami, deduplikacją, dziennikiem).

Jak przetwarzane jest execute

Komenda execute jest odrzucana w całości, jeśli: session_id jest pusty; sesja nie została znaleziona lub nie ma statusu open; sesja należy do innej integracji; sesja wygasła.

Następnie:

  • Rate-limit: do 60 execute na minutę na sesję (okno kroczące). Przekroczenie limitu — odrzucenie.
  • Do 30 metod w jednym execute; nadmiarowe są po cichu odrzucane.
  • Każda metoda jest sprawdzana względem whitelist (zob. Dozwolone metody) — metoda spoza listy jest odrzucana.
  • Chat-guard: jeśli w params jest chat_id lub from_chat_id z wartością różną od 0 i od chat_id sesji — cała metoda jest odrzucana (nie można wysyłać do cudzego czatu). chat_id można w ogóle pominąć: GetMyBot wymuszy czat sesji.
  • Przyciski inline z callback_data są automatycznie tokenizowane (wewnętrzny format x:<token>); czas życia przycisku = do wygaśnięcia sesji. Przyciski url/webapp/switch_inline nie są modyfikowane.

Dozwolone metody

Usługa steruje botem, więc zestaw metod jest ograniczony whitelistą — tylko wysyłka i praca z wiadomościami bieżącego dialogu. Metody spoza listy są po cichu ignorowane (to nie jest błąd).

  • wysyłka: sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction;
  • edytowanie i usuwanie: editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage;
  • odpowiedź na kliknięcie: answerCallbackQuery;
  • przekazywanie i kopiowanie: forwardMessage, copyMessage;
  • przypinanie: pinChatMessage, unpinChatMessage.

Metody na poziomie konta (setWebhook, getUpdates, logOut, close, setMyCommands itd.) nie są dozwolone.

Oczekiwanie i cykl życia sesji

Po każdym execute los sesji zależy od expect i od tego, czy w komendzie były przyciski inline z callback_data:

  • expect = text lub any — kolejna wiadomość użytkownika zostanie przekazana usłudze zdarzeniem message.
  • expect = none I w komendzie NIE było przycisków inline z callback — sesja jest automatycznie zamykana. To „terminalna" wiadomość.
  • expect = none, ALE są przyciski callback — sesja pozostaje otwarta: kliknięcia są przechwytywane, dopóki żyją tokeny przycisków (do TTL).

Ważne: expect nie wpływa na odbiór kliknięć przycisków. Kliknięcia tokenizowanych przycisków są przechwytywane niezależnie od wartości expect — dopóki sesja jest aktywna i token przycisku żyje. expect kontroluje wyłącznie to, czy usługa oczekuje tekstowej/dowolnej odpowiedzi.

Granice dialogu

Kontekst dialogu żyje dokładnie tak długo, jak sesja jest otwarta:

  • Odpowiedź tekstowa przy aktywnym oczekiwaniu (expect = text/any) — trafia do usługi zdarzeniem message, a nie uruchamia zwykłych reakcji bota.
  • Kliknięcie tokenizowanego przycisku — trafia do usługi zdarzeniem callback z oryginalnym callback_data. GetMyBot zawsze sam potwierdza kliknięcie (answerCallbackQuery); sprawdzana jest przynależność kliknięcia (bot + użytkownik + czat sesji). Kliknięcie przycisku nie zamyka sesji.
  • /cancel (a także /cancel@bot i „anuluj") przy aktywnej sesji zewnętrznej — sesja jest zamykana, usłudze wysyłane jest session.cancel, a użytkownikowi „Anulowano.".
  • Jeden dialog na użytkownika. Otwarcie nowej sesji wypiera poprzednią otwartą sesję tego użytkownika — otrzymuje ona session.cancel.
  • Wygaśnięcie TTL. Po upływie timera sesja oznaczana jest jako expired i (jeśli usługa jest online) do usługi wysyłane jest session.expired.

Limity i timeouty

  • Rozmiar ramki: 256 KiB.
  • TTL sesji: domyślnie 3600 sekund (konfigurowane parametrem session_ttl_seconds integracji).
  • Częstotliwość komend: 60 execute na minutę na sesję (okno kroczące).
  • Metod w jednym execute: do 30 (nadmiarowe są odrzucane).
  • Otwartych sesji na integrację: do 1000.
  • Kolejka offline: do 100 zdarzeń na sesję.
  • Heartbeat: ping co 30 sekund; timeout braku aktywności — 60 sekund.
  • Timeout ramki auth: 10 sekund.

Niezawodność (heartbeat, offline, kolejka, fallback)

  • Heartbeat. Serwer co 30 sekund wysyła ramkę {"type":"ping"}. Jeśli od klienta nie ma aktywności przez ponad 60 sekund — połączenie jest zamykane. Klient może wysyłać własne ping/pong w celu utrzymania aktywności. Zerwanie połączenia nie niszczy dialogów: sesje żyją w bazie danych do TTL lub ponownego połączenia.
  • Ponowne połączenie — po stronie usługi. Jeśli połączenie zostało zerwane, Twoja usługa musi połączyć się ponownie. Otwarte sesje w bazie przeżywają restarty do wygaśnięcia TTL.
  • Fallback przy offline na starcie. Jeśli w momencie wyzwolenia reakcji usługa nie jest podłączona — uruchamiana jest reakcja zapasowa z ustawień integracji (on_unavailable_reaction_id). Jeśli nie jest skonfigurowana — nic się nie dzieje (bez błędu).
  • Kolejka offline. Zdarzenia message/callback niedostarczone z powodu offline są umieszczane w kolejce offline (do 100 zdarzeń, FIFO, przechowywana do wygaśnięcia sesji) i dosyłane przy ponownym połączeniu. Zdarzenia session.open/session.cancel/session.expired nie są zapisywane do kolejki.
  • Gwarancja dostarczenia — at-most-once. Przy przepełnieniu lub wygaśnięciu kolejki zdarzenia są odrzucane. Projektuj logikę tak, by pominięcie pojedynczego zdarzenia nie przerywało scenariusza.

Wskaźnik online

Możesz sprawdzić, czy usługa utrzymuje aktywne połączenie, przez endpoint:

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

Odpowiedź:

{ "online": true }

Ten sam wskaźnik dostępny jest na karcie integracji w panelu. Odpowiednik w MCP — narzędzie get_integration_status.

Bezpieczeństwo

GetMyBot wykonuje komendy zewnętrznej usługi własnym tokenem, dlatego ochrona jest rygorystyczna.

  • Brak powierzchni SSRF. GetMyBot jest serwerem WebSocket i sam nie nawiązuje połączeń wychodzących do usługi — to usługa łączy się z GetMyBot. Nie istnieje URL kontrolowany z zewnątrz, pod który platforma by się odwoływała.
  • Token przechowywany jako hash (sha256), weryfikacja w czasie stałym, obsługiwana rotacja.
  • Izolacja dzierżawcy. Komenda jest ściśle powiązana z własną integracją; sesja — z botem + użytkownikiem + czatem. Zdarzenia cudzych botów nie trafiają do Twojego gniazda.
  • Whitelist metod + wymuszony chat_id uniemożliwiają przekształcenie bota w rozsyłacza do dowolnych czatów.
  • Token bota nie jest przekazywany na zewnątrz — usługa nigdy nie komunikuje się bezpośrednio z Telegram.
  • Maskowanie sekretów. Token integracji i wrażliwe wartości są ukrywane w dzienniku i logach.

Przykłady

Minimalny handler: na otwarcie dialogu wysyła wiadomość i zamyka sesję (expect: "none"). Podstaw ID integracji i token. Adres musi koniecznie zawierać <integrationID> w ścieżce.

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: "Cześć od zewnętrznego handlera!" } }],
    }));
  }
});

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": "Cześć od zewnętrznego handlera!"}}
                    ],
                }))

asyncio.run(main())

Przycisk inline i obsługa callback

Aby przechwycić kliknięcie, wyślij przycisk z callback_data (GetMyBot tokenizuje go automatycznie). Przy expect: "none" z przyciskiem sesja pozostaje otwarta, dopóki żyje token przycisku — kliknięcie przyjdzie zdarzeniem callback, w którym event.callback_data jest równe oryginalnej wartości. Potwierdzanie kliknięcia (answerCallbackQuery) nie jest potrzebne — GetMyBot robi to sam.

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "Naciśnij przycisk",
        reply_markup: { inline_keyboard: [[{ text: "Jedziemy", 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: "Przycisk naciśnięty!" } }],
  }));
}

Co dalej