Gestionnaire externe

Le gestionnaire externe est un pont entre votre code et Telegram. Lorsque la logique du constructeur ne suffit pas, vous confiez une réaction distincte à votre serveur : il reçoit des événements et répond par des commandes au format Telegram Bot API, tandis que GetMyBot prend en charge le transport, la livraison et tout ce qui concerne Telegram.

Qu'est-ce que c'est

Dans GetMyBot, les réactions sont construites visuellement : déclencheur → conditions → actions. Une logique complexe ou non standard — votre propre base de données, des calculs, des appels à des systèmes tiers, du ML, des scénarios ramifiés — est difficile à exprimer dans le constructeur. Le gestionnaire externe lève ce plafond : vous connectez votre propre backend et lui confiez le traitement de réactions entières.

Le service externe reçoit de GetMyBot des événements (déclencheur activé, utilisateur a répondu par texte, a appuyé sur un bouton) et commande en retour au bot ce qu'il doit envoyer. GetMyBot exécute les commandes avec son propre token de bot — au-dessus fonctionnent la limitation de débit, les nouvelles tentatives, la déduplication et le journal de dialogue.

Modèle de fonctionnement

  • GetMyBot est le serveur WebSocket. Votre service se connecte à lui de son propre chef et n'a pas besoin d'adresse publique ni d'endpoint webhook. GetMyBot ne fait pas de connexions sortantes vers votre service, il n'y a donc pas de surface d'attaque SSRF.
  • Authentification par token d'intégration. Le service présente le token lors de la connexion ; GetMyBot le compare avec le hash et maintient la connexion.
  • Événements et commandes au format Telegram Bot API. Le service reçoit un événement et répond par un tableau d'appels de type « méthode + paramètres », exactement comme lors du travail avec un bot ordinaire.
  • Les commandes sont exécutées par GetMyBot avec son propre token. Le service ne communique jamais directement avec Telegram : les commandes passent par la validation et entrent dans l'outbox interne de GetMyBot, d'où elles sont envoyées avec le token du bot, avec limitation de débit, nouvelles tentatives, déduplication et journal.

Une seule connexion gère tous les dialogues d'une intégration — les événements de différents utilisateurs sont multiplexés et distingués par le champ session_id.

Configuration de l'intégration

  1. Ouvrez la section Intégrations dans le menu de gauche et cliquez sur Ajouter une intégration.
  2. Sélectionnez le service Gestionnaire externe et donnez-lui un nom.
  3. Après la création, la carte affiche :
    • Adresse WebSocket — de la forme wss://api.mybot.app/ext/ws/<integrationID>, où <integrationID> est l'UUID de cette intégration. C'est à cette adresse que votre service se connecte. Sans l'id dans le chemin, la connexion ne peut pas s'établir.
    • Token de connexion — affiché une seule fois lors de la création. Copiez-le et conservez-le en lieu sûr ; il est impossible de le consulter à nouveau (il n'est stocké que sous forme de hash).
    • Renouvellement du token — ce bouton génère un nouveau token et invalide immédiatement l'ancien. Après le renouvellement, mettez à jour le token dans votre service.
    • Indicateur en ligne — indique si votre service maintient une connexion active en ce moment.

Paramètres supplémentaires de l'intégration, influençant le protocole :

  • session_ttl_seconds — durée de vie de la session en secondes (par défaut 3600).
  • on_unavailable_reaction_id — réaction de secours lancée si le service est hors ligne au moment du déclenchement (voir Fiabilité).

Association à une réaction

Le gestionnaire externe ne se connecte pas via un déclencheur distinct, mais via une action. Dans l'éditeur de réaction, ajoutez l'action Transférer au gestionnaire externe et sélectionnez l'intégration souhaitée.

N'importe quel déclencheur GetMyBot existant peut ouvrir un dialogue — commande, texte, appui sur un bouton, paramètre depuis un lien, requête web entrante, planification. Lorsqu'un tel déclencheur s'active et atteint l'action, GetMyBot ouvre une session proxy et envoie au service l'événement session.open.

Si à ce moment le service n'est pas connecté, GetMyBot lancera la réaction de secours définie dans les paramètres de l'intégration (on_unavailable_reaction_id). Si aucune réaction de secours n'est définie, il ne se passera rien (silencieusement, sans erreur pour l'utilisateur).

Connexion et autorisation

Endpoint : GET /ext/ws/{integrationID} via WebSocket (wss). integrationID est l'UUID de l'intégration de type « Gestionnaire externe ». Limite de taille de trame : 256 KiB.

Il est possible de s'authentifier de deux façons.

1. En-tête lors de la négociation (méthode recommandée). Transmettez le token dans l'en-tête Authorization :

Authorization: Bearer <token>

2. Par trame d'authentification. Si l'en-tête Bearer n'est pas transmis, envoyez dans les 10 secondes suivant l'établissement de la connexion la première trame :

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

Si la trame d'authentification n'est pas reçue dans les 10 secondes, la connexion est fermée avec le code 4401. Un token incorrect dans l'une ou l'autre méthode entraîne également une fermeture 4401.

Le token est généré lors de la création de l'intégration, affiché une seule fois et stocké uniquement sous forme de hash (sha256), la vérification étant à durée constante. Rotation du token :

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

L'endpoint renvoie le nouveau token en texte clair ; l'ancien cesse immédiatement de fonctionner.

Événements : bot → service

Chaque événement est une trame JSON (EventEnvelope). Le champ type est l'un des suivants : session.open, message, callback, session.cancel, session.expired.

Ensemble complet des champs de l'enveloppe :

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Ivan",
    "username": "ivan",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "Telegram Update brut (raw JSON)" },
  "event": { "...": "événement GetMyBot normalisé (raw JSON)" }
}
  • session_id — identifiant de la session (dialogue).
  • bot_id — identifiant du bot.
  • user — données de l'utilisateur : tg_user_id (int64), first_name et username optionnels, ainsi que params — paramètres de l'utilisateur (y compris depuis le lien), map clé→valeur.
  • chat — chat : id (int64) et type (par exemple "private").
  • update — Telegram Update brut (raw JSON). Présent pour session.open, message, callback.
  • event — événement GetMyBot normalisé (raw JSON).

Le contenu varie selon les types :

  • session.open — le déclencheur s'est activé, un nouveau dialogue est ouvert. Contient update (Update brut) et event (événement normalisé complet), user.params est renseigné.
  • message — reçu lorsque le service « maintient une attente » (expect = text/any) et que l'utilisateur a envoyé un message. Le contenu est identique à celui de session.open.
  • callback — l'utilisateur a appuyé sur un bouton précédemment envoyé par le service. update est brut ; dans user, seul tg_user_id est renseigné ; event contient uniquement le callback_data original défini par le service dans le bouton :
{ "callback_data": "<valeur originale définie par le service dans le bouton>" }
  • session.cancel / session.expired — enveloppe minimale : session_id, bot_id, user.tg_user_id, chat.id (pour cancel, également chat.type). Les champs update/event sont absents. Ces événements ne sont délivrés que si le service est en ligne — ils ne sont pas mis en file d'attente hors ligne.

Commandes : service → bot

Une commande est une trame JSON (CommandEnvelope). Le champ type est l'un des suivants : execute, session.close, auth, ping, pong.

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "Bonjour" } }
  ],
  "expect": "none"
}
  • session_id — session à laquelle la commande se rapporte.
  • token — uniquement pour type: "auth" (voir Connexion et autorisation).
  • methods — tableau d'objets { "method": "<nom de la méthode Telegram Bot API>", "params": { ... } }. Le nom de la méthode correspond exactement à celui de Telegram Bot API (par exemple sendMessage), params est l'objet des paramètres de cette méthode.
  • expect — si le service attend une réponse de l'utilisateur : "none", "text" ou "any" (une valeur vide est interprétée comme none).
  • session.close — ferme explicitement le dialogue.

Les commandes ne vont pas directement vers Telegram : GetMyBot les valide et les place dans son outbox, d'où elles sont envoyées avec le token du bot (avec limitation de débit, nouvelles tentatives, déduplication et journal).

Traitement de execute

La commande execute est entièrement abandonnée si : session_id est vide ; la session est introuvable ou n'est pas en statut open ; la session appartient à une autre intégration ; la session a expiré.

Ensuite :

  • Limitation de débit : jusqu'à 60 execute par minute par session (fenêtre glissante). Au-delà de la limite — abandon.
  • Jusqu'à 30 méthodes dans un seul execute ; les méthodes supplémentaires sont silencieusement abandonnées.
  • Chaque méthode est vérifiée par rapport à la liste blanche (voir Méthodes autorisées) — une méthode absente de la liste est rejetée.
  • Protection de chat : si params contient chat_id ou from_chat_id avec une valeur différente de 0 et différente du chat_id de la session, la méthode entière est rejetée (il est interdit d'envoyer dans un chat étranger). chat_id peut ne pas être spécifié du tout : GetMyBot y substituera de force le chat de la session.
  • Les boutons inline avec callback_data sont automatiquement tokenisés (format interne x:<token>) ; la durée de vie du bouton est valable jusqu'à l'expiration de la session. Les boutons url/webapp/switch_inline ne sont pas modifiés.

Méthodes autorisées

Le service commande le bot, donc l'ensemble des méthodes est limité à une liste blanche — uniquement l'envoi et la gestion des messages du dialogue en cours. Les méthodes absentes de la liste sont silencieusement ignorées (ce n'est pas une erreur).

  • envoi : sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction ;
  • modification et suppression : editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage ;
  • réponse à un appui : answerCallbackQuery ;
  • transfert et copie : forwardMessage, copyMessage ;
  • épinglage : pinChatMessage, unpinChatMessage.

Les méthodes de niveau compte (setWebhook, getUpdates, logOut, close, setMyCommands, etc.) ne sont pas autorisées.

Attente et cycle de vie de la session

Après chaque execute, le devenir de la session dépend de expect et de la présence de boutons inline avec callback_data dans la commande :

  • expect = text ou any — le prochain message de l'utilisateur sera transmis au service via l'événement message.
  • expect = none ET la commande ne contenait PAS de boutons inline avec callback — la session se ferme automatiquement. C'est un message « terminal ».
  • expect = none, MAIS des boutons callback sont présents — la session reste ouverte : les appuis sont capturés tant que les tokens des boutons sont valides (jusqu'au TTL).

Important : expect n'influence pas la réception des appuis sur les boutons. Les appuis sur les boutons tokenisés sont capturés indépendamment de la valeur de expect — tant que la session et le token du bouton sont valides. expect contrôle uniquement le fait que le service attend ou non une réponse textuelle/quelconque.

Frontières du dialogue

Le contexte du dialogue vit exactement jusqu'à la fermeture de la session :

  • Réponse textuelle lors d'une attente active (expect = text/any) — est transmise au service via l'événement message, et non via les réactions habituelles du bot.
  • Appui sur un bouton tokenisé — est transmis au service via l'événement callback avec le callback_data original. GetMyBot confirme toujours lui-même l'appui (answerCallbackQuery) ; l'appartenance de l'appui est vérifiée (bot + utilisateur + chat de la session). Un appui sur un bouton ne ferme pas la session.
  • /cancel (ainsi que /cancel@bot et « annuler ») lors d'une session externe active — la session se ferme, session.cancel est envoyé au service, « Annulé. » est envoyé à l'utilisateur.
  • Un dialogue par utilisateur. L'ouverture d'une nouvelle session supplante l'ancienne session ouverte de cet utilisateur — session.cancel lui est envoyé.
  • Expiration du TTL. La session est marquée expired par le minuteur et (si le service est en ligne) session.expired lui est envoyé.

Limites et délais d'expiration

  • Taille de trame : 256 KiB.
  • TTL de session : 3600 secondes par défaut (configurable via le paramètre session_ttl_seconds de l'intégration).
  • Fréquence des commandes : 60 execute par minute par session (fenêtre glissante).
  • Méthodes dans un seul execute : jusqu'à 30 (les méthodes supplémentaires sont abandonnées).
  • Sessions ouvertes par intégration : jusqu'à 1000.
  • File d'attente hors ligne : jusqu'à 100 événements par session.
  • Heartbeat : ping toutes les 30 secondes ; délai d'expiration en cas d'inactivité — 60 secondes.
  • Délai d'expiration de la trame d'authentification : 10 secondes.

Fiabilité (heartbeat, hors ligne, file d'attente, fallback)

  • Heartbeat. Le serveur envoie une trame {"type":"ping"} toutes les 30 secondes. Si le client n'a pas eu d'activité depuis plus de 60 secondes, la connexion est fermée. Le client peut envoyer ses propres trames ping/pong pour maintenir l'activité. Une interruption de connexion ne tue pas les dialogues : les sessions survivent en base de données jusqu'au TTL ou à la reconnexion.
  • La reconnexion est à la charge du service. Si la connexion est interrompue, votre service doit se reconnecter. Les sessions ouvertes en base de données survivent aux redémarrages jusqu'à l'expiration du TTL.
  • Fallback en cas d'absence lors du démarrage. Si le service n'est pas connecté au moment du déclenchement de la réaction, la réaction de secours définie dans les paramètres de l'intégration est lancée (on_unavailable_reaction_id). Si elle n'est pas définie, il ne se passe rien (silencieusement).
  • File d'attente hors ligne. Les événements message/callback non délivrés en raison d'une absence sont placés dans la file d'attente hors ligne (jusqu'à 100 événements, FIFO, conservés jusqu'à l'expiration de la session) et renvoyés lors de la reconnexion. Les événements session.open/session.cancel/session.expired ne sont pas mis en file d'attente.
  • Garantie de livraison — at-most-once. En cas de dépassement ou d'expiration de la file d'attente, les événements sont abandonnés. Concevez votre logique de façon à ce que la perte d'un événement isolé ne brise pas le scénario.

Indicateur en ligne

Il est possible de vérifier si le service maintient une connexion active via l'endpoint :

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

Réponse :

{ "online": true }

Le même indicateur est disponible sur la carte d'intégration dans le tableau de bord. L'équivalent en MCP est l'outil get_integration_status.

Sécurité

GetMyBot exécute les commandes d'un service tiers avec son propre token, la protection est donc stricte.

  • Pas de surface d'attaque SSRF. GetMyBot agit en tant que serveur WebSocket et ne fait pas lui-même de connexions sortantes vers le service — c'est le service qui se connecte à GetMyBot. Il n'y a pas d'URL contrôlable depuis l'extérieur vers laquelle la plateforme se rendrait.
  • Le token est stocké sous forme de hash (sha256), la vérification est à durée constante, la rotation est prise en charge.
  • Isolation des locataires. Une commande est strictement liée à sa propre intégration ; une session est liée au bot + utilisateur + chat. Les événements de bots tiers n'arrivent pas dans votre socket.
  • Liste blanche des méthodes + chat_id forcé empêchent de transformer le bot en expéditeur vers des chats arbitraires.
  • Le token du bot n'est pas transmis à l'extérieur — le service ne communique jamais directement avec Telegram.
  • Masquage des secrets. Le token d'intégration et les valeurs sensibles sont masqués dans le journal et les logs.

Exemples

Gestionnaire minimal : à l'ouverture d'un dialogue, envoie un message et ferme la session (expect: "none"). Remplacez l'id de l'intégration et le token. L'adresse doit obligatoirement contenir <integrationID> dans le chemin.

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: "Bonjour depuis le gestionnaire externe !" } }],
    }));
  }
});

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": "Bonjour depuis le gestionnaire externe !"}}
                    ],
                }))

asyncio.run(main())

Bouton inline et traitement du callback

Pour capturer un appui, envoyez un bouton avec callback_data (GetMyBot le tokenise automatiquement). Avec expect: "none" et un bouton, la session reste ouverte tant que le token du bouton est valide — l'appui arrivera via l'événement callback, dans lequel event.callback_data est égal à la valeur originale. Il n'est pas nécessaire de confirmer l'appui (answerCallbackQuery) — GetMyBot le fait lui-même.

if (ev.type === "session.open") {
  ws.send(JSON.stringify({
    type: "execute",
    session_id: ev.session_id,
    expect: "none",
    methods: [{
      method: "sendMessage",
      params: {
        text: "Appuyez sur le bouton",
        reply_markup: { inline_keyboard: [[{ text: "C'est parti", 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: "Bouton appuyé !" } }],
  }));
}

Et ensuite