外部ハンドラー
外部ハンドラーは、あなたのコードと 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 フィールドで区別されます。
連携の設定
- 左メニューの連携セクションを開き、連携を追加をクリックします。
- 外部ハンドラーサービスを選択し、名前を設定します。
- 作成後、カードに以下が表示されます。
- WebSocket アドレス —
wss://api.mybot.app/ext/ws/<integrationID>の形式。<integrationID>はこの連携の UUID です。あなたのサービスはこれに接続します。パスに ID がない場合、接続は確立されません。 - 接続トークン — 作成時に一度だけ表示されます。コピーして安全な場所に保管してください。トークンは再確認できません(ハッシュのみで保存されます)。
- トークンの再発行 — ボタンをクリックすると新しいトークンが生成され、古いトークンはすぐに無効化されます。再発行後はサービス内のトークンを更新してください。
- オンラインインジケーター — あなたのサービスが現在アクティブな接続を維持しているかどうかを示します。
- WebSocket アドレス —
プロトコルに影響する追加の連携パラメータ:
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.open、message、callback、session.cancel、session.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_name、username、およびparams(ユーザーパラメータ、リンクからのものを含む、key→value マップ)。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 に入れ、ボットトークンでレート制限、リトライ、重複排除、ログ付きで送信します。
execute の処理
execute コマンドは以下の場合に全体がドロップされます:session_id が空、セッションが見つからないまたは open 状態でない、セッションが別の連携に属している、セッションが期限切れ。
その後:
- レート制限: セッションあたり 1 分間に最大 60 回の
execute(スライディングウィンドウ)。超過分はドロップされます。 - 1 つの
executeあたり最大 30 メソッド。超過分は静かに破棄されます。 - 各メソッドはホワイトリスト(許可されたメソッドを参照)に対して確認されます。リスト外のメソッドは拒否されます。
- Chat-guard:
paramsにchat_idまたはfrom_chat_idがあり、0でなくセッションのchat_idと異なる値が設定されている場合、そのメソッド全体が拒否されます(他のチャットに送信できません)。chat_idは省略可能です。GetMyBot がセッションのチャットを強制的に設定します。 callback_data付きのインラインボタンは自動的にトークン化されます(内部形式x:<token>)。ボタンの有効期限 = セッションの TTL 期限まで。url/webapp/switch_inlineボタンはそのまま維持されます。
許可されたメソッド
サービスはボットにコマンドを送信するため、メソッドセットはホワイトリストに制限されています。現在のダイアログの送信とメッセージ操作のみが許可されます。リスト外のメソッドは静かに無視されます(エラーではありません)。
- 送信:
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 とコマンドに 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: "ボタンが押されました!" } }],
}));
}
次のステップ
- 連携 — 接続可能なサービスの概要。
- リアクション — トリガー、条件、アクションの組み立て方。
- Web リクエストと Webhook — ダイアログなしで外部 URL を呼び出すシンプルな方法。