外部ハンドラー

外部ハンドラーは、あなたのコードと Telegram の橋渡しをします。ビルダーのロジックでは不十分な場合、特定のリアクションを独自のサーバーに委ねることができます。サーバーはイベントを受け取り、Telegram Bot API 形式でコマンドを返します。GetMyBot がトランスポート、配信、Telegram の通信すべてを担当します。

概要

GetMyBot のリアクションはビジュアルに作成します:トリガー → 条件 → アクション。複雑または非標準のロジック(独自のデータベース、計算、外部システムとの通信、ML、複雑な分岐シナリオ)はビルダーでは表現が難しい場合があります。外部ハンドラーはこの制限を取り除きます。独自のバックエンドを接続し、リアクション全体の処理を委ねることができます。

外部サービスは GetMyBot からイベント(トリガーが発火した、ユーザーがテキストを返信した、ボタンをクリックした)を受け取り、ボットに送信する内容をコマンドで返します。GetMyBot はボットトークンでコマンドを実行します。レート制限、リトライ、重複排除、ダイアログログが機能します。

動作モデル

  • GetMyBot は WebSocket サーバーです。 あなたのサービスが GetMyBot に接続し、公開アドレスや Webhook エンドポイントは不要です。GetMyBot からあなたのサービスへの発信接続は行われないため、SSRF サーフェスがありません。
  • 認証は連携トークンで行います。 サービスは接続時にトークンを提示し、GetMyBot がそのハッシュと照合して接続を維持します。
  • イベントとコマンドは Telegram Bot API 形式です。 サービスはイベントを受け取り、通常のボット操作と同様に「メソッド + パラメータ」形式の呼び出し配列で応答します。
  • コマンドは GetMyBot がボットトークンで実行します。 サービスは Telegram と直接通信しません。コマンドはバリデーションを経て GetMyBot の内部 outbox に入り、レート制限、リトライ、重複排除、ログ付きでボットトークンから送信されます。

1 つの接続が 1 つの連携のすべてのダイアログを処理します。異なるユーザーのイベントは多重化され、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 の既存のトリガー(コマンド、テキスト、ボタンクリック、リンクのパラメータ、受信 Web リクエスト、スケジュール)で開始できます。そのトリガーが発火してアクションに到達すると、GetMyBot はプロキシセッションを開き、サービスに session.open イベントを送信します。

この時点でサービスが接続されていない場合、GetMyBot は連携設定のフォールバックリアクション(on_unavailable_reaction_id)を起動します。フォールバックが設定されていない場合は何も起こりません(ユーザーにはエラーなしで静かに処理されます)。

接続と認証

エンドポイント:WebSocket(wss)上の GET /ext/ws/{integrationID}integrationID は「外部ハンドラー」タイプの連携の UUID です。フレームサイズの制限は 256 KiB です。

認証は 2 つの方法のいずれかで行えます。

1. ハンドシェイク時のヘッダー(推奨)。 Authorization ヘッダーでトークンを渡します:

Authorization: Bearer <token>

2. 認証フレームで。 Bearer ヘッダーが送信されない場合、接続確立後 10 秒以内に最初のフレームとして以下を送信します:

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

10 秒以内に認証フレームが来ない場合、接続はコード 4401 で閉じられます。いずれの方法でも無効なトークンはコード 4401 で切断されます。

トークンは連携作成時に生成され、一度だけ表示され、ハッシュ(sha256)のみで保存されます。照合は constant-time で行われます。トークンのローテーション:

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

エンドポイントは新しいプレーンテキストトークンを返します。古いトークンはすぐに無効になります。

イベント: ボット → サービス

各イベントは JSON フレーム(EventEnvelope)です。type フィールドは次のいずれか:session.openmessagecallbacksession.cancelsession.expired

エンベロープの完全なフィールドセット:

{
  "type": "session.open",
  "session_id": "string",
  "bot_id": "string",
  "user": {
    "tg_user_id": 123456789,
    "first_name": "Taro",
    "username": "taro",
    "params": { "utm": "promo" }
  },
  "chat": { "id": 123456789, "type": "private" },
  "update": { "...": "raw Telegram Update (raw JSON)" },
  "event": { "...": "normalized GetMyBot event (raw JSON)" }
}
  • session_id — セッション(ダイアログ)の識別子。
  • bot_id — ボットの識別子。
  • user — ユーザーデータ:tg_user_id(int64)、オプションの first_nameusername、および params(ユーザーパラメータ、リンクからのものを含む、key→value マップ)。
  • chat — チャット:id(int64)と type(例:"private")。
  • update — 生の Telegram Update(raw JSON)。session.openmessagecallback に存在します。
  • 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_idbot_iduser.tg_user_idchat.idcancel の場合は chat.type も含む)。update/event フィールドはありません。これらのイベントはサービスがオンラインの場合のみ配信されます。オフラインキューには入りません。

コマンド: サービス → ボット

コマンドは JSON フレーム(CommandEnvelope)です。type フィールドは次のいずれか:executesession.closeauthpingpong

{
  "type": "execute",
  "session_id": "string",
  "methods": [
    { "method": "sendMessage", "params": { "text": "こんにちは" } }
  ],
  "expect": "none"
}
  • session_id — コマンドが属するセッション。
  • tokentype: "auth" の場合のみ(接続と認証を参照)。
  • methods{ "method": "<Telegram Bot API メソッド名>", "params": { ... } } オブジェクトの配列。メソッド名は Telegram Bot API と完全に同じ(例:sendMessage)、params はそのメソッドのパラメータオブジェクトです。
  • expect — サービスがユーザーの返答を待つかどうか:"none""text"、または "any"(空の値は none として扱われます)。
  • session.close — ダイアログを明示的に閉じます。

コマンドは Telegram に直接送信されません。GetMyBot がバリデーションを行い、内部 outbox に入れ、ボットトークンでレート制限、リトライ、重複排除、ログ付きで送信します。

execute の処理

execute コマンドは以下の場合に全体がドロップされます:session_id が空、セッションが見つからないまたは open 状態でない、セッションが別の連携に属している、セッションが期限切れ。

その後:

  • レート制限: セッションあたり 1 分間に最大 60 回の execute(スライディングウィンドウ)。超過分はドロップされます。
  • 1 つの execute あたり最大 30 メソッド。超過分は静かに破棄されます。
  • 各メソッドはホワイトリスト(許可されたメソッドを参照)に対して確認されます。リスト外のメソッドは拒否されます。
  • Chat-guard: paramschat_id または from_chat_id があり、0 でなくセッションの chat_id と異なる値が設定されている場合、そのメソッド全体が拒否されます(他のチャットに送信できません)。chat_id は省略可能です。GetMyBot がセッションのチャットを強制的に設定します。
  • callback_data 付きのインラインボタンは自動的にトークン化されます(内部形式 x:<token>)。ボタンの有効期限 = セッションの TTL 期限まで。url/webapp/switch_inline ボタンはそのまま維持されます。

許可されたメソッド

サービスはボットにコマンドを送信するため、メソッドセットはホワイトリストに制限されています。現在のダイアログの送信とメッセージ操作のみが許可されます。リスト外のメソッドは静かに無視されます(エラーではありません)。

  • 送信:sendMessagesendPhotosendDocumentsendVideosendAudiosendMediaGroupsendAnimationsendVoicesendLocationsendChatAction
  • 編集と削除:editMessageTexteditMessageCaptioneditMessageReplyMarkupdeleteMessage
  • クリックへの返答:answerCallbackQuery
  • 転送とコピー:forwardMessagecopyMessage
  • ピン留め:pinChatMessageunpinChatMessage

アカウントレベルのメソッド(setWebhookgetUpdateslogOutclosesetMyCommands など)は許可されていません

待機とセッションのライフサイクル

execute 後のセッションの状態は expect とコマンドに callback_data 付きのインラインボタンがあったかどうかによって異なります:

  • expect = text または any — ユーザーの次のメッセージが message イベントとしてサービスに転送されます。
  • expect = none かつコマンドに callback 付きインラインボタンがなかった場合 — セッションは自動的に閉じられます。これが「終端」メッセージです。
  • expect = noneしかし callback ボタンがある場合 — セッションは開いたまま:ボタントークンが有効な間(TTL まで)クリックがキャッチされます。

重要:expect はボタンクリックの受け取りに影響しません。 トークン化されたボタンのクリックは expect の値に関わらずキャッチされます。セッションとボタントークンが有効な間は機能します。expectテキスト/任意の返答をサービスが待つかどうかのみを制御します。

ダイアログの境界

ダイアログコンテキストはセッションが開いている間のみ有効です:

  • アクティブな待機中のテキスト返答expect = text/any)— 通常のボットリアクションをトリガーせずに message イベントとしてサービスに送信されます。
  • トークン化されたボタンのクリック — オリジナルの callback_data を含む callback イベントとしてサービスに送信されます。GetMyBot は常に自動的にクリックを確認します(answerCallbackQuery)。所有権の確認(ボット + ユーザー + セッションのチャット)が行われます。ボタンクリックでセッションは閉じられません
  • /cancel/cancel@bot および「キャンセル」も)— アクティブな外部セッション中はセッションが閉じられ、サービスに session.cancel が送信され、ユーザーに「キャンセルされました。」が送信されます。
  • 1 ユーザーあたり 1 つのダイアログ。 新しいセッションを開くと、そのユーザーの以前の開いているセッションが置き換えられ、置き換えられたセッションに session.cancel が送信されます。
  • TTL の期限切れ。 タイマーによりセッションが expired とマークされ、サービスがオンラインの場合は session.expired が送信されます。

制限とタイムアウト

  • フレームサイズ: 256 KiB。
  • セッション TTL: デフォルト 3600 秒(連携の session_ttl_seconds パラメータで設定可能)。
  • コマンド頻度: セッションあたり 1 分間に 60 回の execute(スライディングウィンドウ)。
  • 1 つの execute あたりのメソッド数: 最大 30(超過分は破棄)。
  • 連携あたりの開いているセッション数: 最大 1000。
  • オフラインキュー: セッションあたり最大 100 イベント。
  • ハートビート: 30 秒ごとに ping。非アクティブタイムアウト — 60 秒。
  • 認証フレームのタイムアウト: 10 秒。

信頼性(ハートビート、オフライン、キュー、フォールバック)

  • ハートビート。 サーバーは 30 秒ごとに {"type":"ping"} フレームを送信します。クライアントから 60 秒以上アクティビティがない場合、接続は閉じられます。クライアントは独自の ping/pong を送信してアクティビティを維持できます。接続切断でダイアログは終了しません。セッションは TTL またはの再接続まで DB に保持されます。
  • 再接続はサービス側の責任です。 接続が切れた場合、あなたのサービスが再接続する必要があります。DB 内の開いているセッションは TTL が切れるまで再起動を経ても維持されます。
  • オフライン時のフォールバック。 リアクションのトリガー発火時にサービスが接続されていない場合、連携設定のフォールバックリアクション(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 で照合、ローテーション対応。
  • テナント分離。 コマンドは固有の連携に厳密に紐付けられます。セッションはボット + ユーザー + チャットに紐付けられます。他のボットのイベントがあなたのソケットに入ることはありません。
  • メソッドホワイトリスト + 強制 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())

インラインボタンと 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: "ボタンが押されました!" } }],
  }));
}

次のステップ