외부 핸들러
외부 핸들러는 자체 코드와 Telegram 사이의 브리지입니다. 구성기의 로직으로 충분하지 않을 때, 특정 반응의 처리를 자체 서버에 위임할 수 있습니다. 서버는 Telegram Bot API 형식으로 이벤트를 수신하고 명령으로 응답하며, GetMyBot이 전송, 배달, 모든 Telegram 처리를 담당합니다.
개요
GetMyBot의 반응은 시각적으로 구성됩니다: 트리거 → 조건 → 액션. 복잡하거나 비표준적인 로직 — 자체 데이터베이스, 계산, 외부 시스템 연동, ML, 복잡한 분기 시나리오 — 은 구성기로 표현하기 어렵습니다. 외부 핸들러는 이 한계를 제거합니다. 자체 백엔드를 연결하고 전체 반응의 처리를 위임할 수 있습니다.
외부 서비스는 GetMyBot으로부터 이벤트(트리거 실행, 사용자 텍스트 응답, 버튼 클릭)를 수신하고, 봇에 무엇을 전송할지 명령으로 응답합니다. GetMyBot이 봇 토큰으로 명령을 실행하며 — rate-limit, 재시도, 중복 제거, 대화 로그가 모두 적용됩니다.
작동 방식
- GetMyBot이 WebSocket 서버입니다. 서비스가 직접 연결하므로 공개 주소나 webhook 엔드포인트가 필요하지 않습니다. GetMyBot은 서비스로 아웃바운드 연결을 하지 않으므로 SSRF 공격 면적이 없습니다.
- 인증은 통합 토큰으로 합니다. 서비스가 연결 시 토큰을 제시하면 GetMyBot이 해시와 비교하여 연결을 유지합니다.
- 이벤트와 명령은 Telegram Bot API 형식입니다. 서비스는 이벤트를 수신하고, 「메서드 + 매개변수」 형태의 호출 배열로 응답합니다. 일반 봇 작업과 동일합니다.
- 명령은 GetMyBot이 자체 토큰으로 실행합니다. 서비스는 Telegram에 직접 연결하지 않습니다. 명령은 유효성 검사를 거쳐 GetMyBot의 내부 outbox에 들어가며, 봇 토큰으로 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)의 대체 반응을 시작합니다. 대체 반응이 설정되지 않은 경우 아무 일도 일어나지 않습니다(사용자에게 오류 없이 조용히 처리됨).
연결 및 인증
엔드포인트: WebSocket(wss) 위의 GET /ext/ws/{integrationID}. integrationID는 「외부 핸들러」 유형 통합의 UUID입니다. 프레임 크기 제한: 256 KiB.
두 가지 인증 방법 중 하나를 사용할 수 있습니다.
1. 핸드셰이크 시 헤더(권장). Authorization 헤더로 토큰을 전달합니다.
Authorization: Bearer <token>
2. Auth 프레임. Bearer 헤더를 전달하지 않은 경우, 연결 수립 후 10초 이내에 첫 번째 프레임으로 다음을 전송합니다.
{ "type": "auth", "token": "<token>" }
10초 안에 auth 프레임이 도착하지 않으면 연결이 코드 4401로 닫힙니다. 두 방법 모두 잘못된 토큰은 4401 close 처리됩니다.
토큰은 통합 생성 시 생성되며, 한 번만 표시되고 해시(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": "Ivan",
"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— 사용자 매개변수(링크의 매개변수 포함), 키→값 맵.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(슬라이딩 윈도우). 한도 초과 시 삭제됩니다. - 하나의
execute에 최대 30개 메서드; 초과분은 자동 삭제됩니다. - 각 메서드는 화이트리스트 확인(허용된 메서드 참조) — 목록에 없는 메서드는 거부됩니다.
- Chat-guard:
params에chat_id또는from_chat_id가 있고 그 값이0이 아니면서 세션의chat_id와 다를 경우 — 해당 메서드 전체가 거부됩니다(다른 채팅으로 전송 불가).chat_id를 생략하면 GetMyBot이 세션의 채팅을 강제로 설정합니다. callback_data가 있는 인라인 버튼은 자동으로 토큰화됩니다(내부 형식x:<token>). 버튼 유효 시간 = 세션 만료 시까지.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이 전송되며, 사용자에게 「취소됨.」이 전송됩니다.- 사용자당 하나의 대화. 새 세션을 열면 해당 사용자의 기존 열린 세션이 교체됩니다 —
session.cancel이 전송됩니다. - TTL 만료. 타이머에 의해 세션이
expired로 표시되고, 서비스가 온라인이면session.expired가 전송됩니다.
한도 및 타임아웃
- 프레임 크기: 256 KiB.
- 세션 TTL: 기본값 3600초(통합
session_ttl_seconds매개변수로 설정 가능). - 명령 빈도: 세션당 분당 60개의
execute(슬라이딩 윈도우). - 하나의
execute의 메서드 수: 최대 30개(초과분 삭제). - 통합당 열린 세션 수: 최대 1000개.
- 오프라인 큐: 세션당 최대 100개 이벤트.
- Heartbeat: 30초마다
ping; 비활성 타임아웃 60초. - Auth 프레임 타임아웃: 10초.
신뢰성(heartbeat, 오프라인, 큐, fallback)
- Heartbeat. 서버가 30초마다
{"type":"ping"}프레임을 전송합니다. 클라이언트로부터 60초 이상 활동이 없으면 연결이 닫힙니다. 클라이언트도 활성 상태 유지를 위해 자체ping/pong을 전송할 수 있습니다. 연결 끊김이 대화를 종료시키지 않습니다. 세션은 TTL이 만료되거나 재연결될 때까지 DB에 유지됩니다. - 재연결은 서비스 쪽에서 처리합니다. 연결이 끊기면 서비스가 다시 연결해야 합니다. DB의 열린 세션은 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 비교, 교체 지원.
- 테넌트 격리. 명령은 자체 통합에 엄격히 바인딩됩니다. 세션은 봇 + 사용자 + 채팅에 바인딩됩니다. 다른 봇의 이벤트는 소켓에 들어오지 않습니다.
- 메서드 화이트리스트 + 강제
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: "버튼이 클릭되었습니다!" } }],
}));
}
다음 단계
- 통합 — 연결 가능한 서비스 개요.
- 반응 — 트리거, 조건, 액션을 구성하는 방법.
- 웹 요청 및 webhook — 대화 없이 외부 URL을 호출하는 더 간단한 방법.