מטפל חיצוני

מטפל חיצוני הוא גשר בין הקוד שלכם ל-Telegram. כאשר לוגיקת הכלי אינה מספיקה, אתם מוסרים ריאקציה שלמה לשרת שלכם: הוא מקבל אירועים ועונה בפקודות בפורמט Telegram Bot API, בעוד GetMyBot מטפל בתעבורה, בהעברה ובכל מה ש-Telegram.

מה זה

ריאקציות ב-GetMyBot נבנות ויזואלית: טריגר ← תנאים ← פעולות. לוגיקה מורכבת או לא סטנדרטית — מסד הנתונים שלכם, חישובים, קריאות למערכות חיצוניות, ML, תסריטים מסועפים — קשה לבטא בכלי. המטפל החיצוני מסיר תקרה זו: אתם מחברים בק-אנד משלכם ומוסרים לו ריאקציות שלמות לעיבוד.

השירות החיצוני מקבל מ-GetMyBot אירועים (טריגר הופעל, המשתמש ענה בטקסט, לחץ על כפתור) ובתגובה מורה לבוט מה לשלוח. GetMyBot מבצע את הפקודות בטוקן הבוט שלו — מעל זה פועלים rate-limit, ניסיונות חוזרים, כפילויות, יומן דיאלוג.

מודל עבודה

  • GetMyBot הוא שרת WebSocket. השירות שלכם מתחבר אליו בעצמו ואינו זקוק לכתובת ציבורית או ל-webhook endpoint. GetMyBot אינו יוצר חיבורים יוצאים לשירות שלכם, לכן אין משטח SSRF.
  • אימות — בטוקן אינטגרציה. השירות מציג טוקן בעת החיבור; GetMyBot מאמת אותו מול ה-hash ומשמר את החיבור.
  • אירועים ופקודות — בפורמט Telegram Bot API. השירות מקבל אירוע ועונה במערך קריאות בסגנון "method + params", בדיוק כמו עבודה עם בוט רגיל.
  • פקודות מבצע GetMyBot בטוקן שלו. השירות לעולם אינו פונה ל-Telegram ישירות: פקודות עוברות ולידציה ומגיעות ל-outbox הפנימי של GetMyBot, משם הן נשלחות בטוקן הבוט עם rate-limit, ניסיונות חוזרים, כפילויות ויומן.

חיבור אחד משרת את כל הדיאלוגים של אינטגרציה אחת — אירועי משתמשים שונים מוכפלים ומובחנים על פי שדה session_id.

הגדרת אינטגרציה

  1. פתחו את הסעיף אינטגרציות בתפריט שמאל ולחצו הוסף אינטגרציה.
  2. בחרו שירות מטפל חיצוני והגדירו שם.
  3. לאחר היצירה הכרטיס יציג:
    • כתובת WebSocket — בסגנון wss://api.mybot.app/ext/ws/<integrationID>, כאשר <integrationID> הוא ה-UUID של אינטגרציה זו. השירות שלכם מתחבר לפיה. ללא ה-ID בנתיב החיבור לא יתבסס.
    • טוקן חיבור — מוצג פעם אחת בעת היצירה. העתיקו ושמרו אותו במקום בטוח; לא ניתן לצפות בטוקן שוב (הוא נשמר רק כ-hash).
    • חידוש טוקן — הכפתור מייצר טוקן חדש ומיד מבטל את הישן. לאחר חידוש עדכנו את הטוקן בשירות שלכם.
    • אינדיקטור מחובר — מציג האם השירות שלכם מחזיק חיבור פעיל כרגע.

פרמטרים נוספים של אינטגרציה המשפיעים על הפרוטוקול:

  • session_ttl_seconds — זמן חיים של סשן בשניות (ברירת מחדל 3600).
  • on_unavailable_reaction_id — ריאקציה חלופית שמופעלת אם השירות אופליין בעת ההפעלה (ראו אמינות).

קישור לריאקציה

המטפל החיצוני אינו מתחבר כטריגר נפרד, אלא כפעולה. בעורך הריאקציה הוסיפו פעולת העבר למטפל חיצוני ובחרו את האינטגרציה הרצויה.

פתיחת דיאלוג יכולה להיות על ידי כל טריגר קיים ב-GetMyBot — פקודה, טקסט, לחיצת כפתור, פרמטר מקישור, בקשת ווב נכנסת, לוח זמנים. כאשר טריגר כזה מופעל ומגיע לפעולה, GetMyBot פותח סשן proxy ושולח לשירות אירוע session.open.

אם באותו רגע השירות אינו מחובר, GetMyBot יפעיל את הריאקציה החלופית מהגדרות האינטגרציה (on_unavailable_reaction_id). אם לא הוגדרה חלופית — לא יקרה דבר (בשקט, ללא שגיאה למשתמש).

חיבור ואימות

נקודת קצה: GET /ext/ws/{integrationID} מעל WebSocket (wss). integrationID — UUID של אינטגרציה מסוג "מטפל חיצוני". מגבלת גודל frame — 256 KiB.

ניתן לאמת באחת משתי דרכים.

1. כותרת בלחיצת יד (מועדף). העבירו את הטוקן בכותרת Authorization:

Authorization: Bearer <token>

2. Frame auth. אם כותרת Bearer לא הועברה, שלחו כ-frame הראשון תוך 10 שניות מביסוס החיבור:

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

אם ה-auth frame לא הגיע תוך 10 שניות — החיבור נסגר עם קוד 4401. טוקן שגוי בכל אחת מהדרכים — גם close 4401.

הטוקן נוצר בעת יצירת האינטגרציה, מוצג פעם אחת ונשמר רק כ-hash (sha256), האימות — בזמן קבוע (constant-time). סיבוב טוקן:

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

נקודת הקצה מחזירה טוקן plaintext חדש; הישן מפסיק לעבוד מיד.

אירועים: בוט ← שירות

כל אירוע הוא JSON frame (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 — פרמטרי משתמש (כולל מקישור), map מפתח←ערך.
  • 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 frame (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": "<שם method ב-Telegram Bot API>", "params": { ... } }. שם ה-method — בדיוק כמו ב-Telegram Bot API (לדוגמה sendMessage), params — אובייקט פרמטרי ה-method.
  • expect — האם השירות מחכה לתשובת משתמש: "none", "text" או "any" (ערך ריק נחשב כ-none).
  • session.close — לסגור דיאלוג באופן מפורש.

פקודות אינן הולכות ל-Telegram ישירות: GetMyBot מאמת אותן ומכניס ל-outbox שלו, משם הן נשלחות בטוקן הבוט (עם rate-limit, ניסיונות חוזרים, כפילויות, יומן).

כיצד execute מעובד

הפקודה execute נדחית כולה אם: session_id ריק; הסשן לא נמצא או אינו בסטטוס open; הסשן שייך לאינטגרציה אחרת; הסשן פג.

לאחר מכן:

  • Rate-limit: עד 60 execute לדקה לסשן (חלון נגלל). מעל המגבלה — נדחה.
  • עד 30 methods ב-execute אחד; עודפים נדחים בשקט.
  • כל method נבדק מול ה-whitelist (ראו Methods מורשים) — method שאינו ברשימה נדחה.
  • Chat-guard: אם ב-params יש chat_id או from_chat_id עם ערך שאינו 0 ואינו שווה ל-chat_id של הסשן — כל ה-method נדחה (אין לשלוח לצ'אט אחר). chat_id ניתן לא לציין בכלל: GetMyBot יוסיף את צ'אט הסשן בכפייה.
  • כפתורים inline עם callback_data עוברים טוקניזציה אוטומטית (פורמט פנימי x:<token>); זמן חיים של כפתור = עד תפוגת הסשן. כפתורי url/webapp/switch_inline אינם נגעים.

Methods מורשים

השירות מפקד על הבוט, לכן ערכת ה-methods מוגבלת ל-whitelist — רק שליחה ועבודה עם הודעות הדיאלוג הנוכחי. Methods שאינם ברשימה נדחים בשקט (זו אינה שגיאה).

  • שליחה: sendMessage, sendPhoto, sendDocument, sendVideo, sendAudio, sendMediaGroup, sendAnimation, sendVoice, sendLocation, sendChatAction;
  • עריכה ומחיקה: editMessageText, editMessageCaption, editMessageReplyMarkup, deleteMessage;
  • תגובה ללחיצה: answerCallbackQuery;
  • העברה והעתקה: forwardMessage, copyMessage;
  • הצמדה: pinChatMessage, unpinChatMessage.

Methods ברמת חשבון (setWebhook, getUpdates, logOut, close, setMyCommands וכד') אינם מורשים.

ציפייה ומחזור חיים של סשן

לאחר כל execute גורל הסשן תלוי ב-expect ובנוכחות כפתורים inline עם callback_data:

  • expect = text או any — ההודעה הבאה של המשתמש תועבר לשירות כאירוע message.
  • expect = none ולא היו כפתורי callback בפקודה — הסשן נסגר אוטומטית. זוהי הודעה "סופנית".
  • expect = none, אך יש כפתורי callback — הסשן נשאר פתוח: לחיצות נלכדות כל עוד טוקני הכפתורים חיים (עד ה-TTL).

חשוב: expect אינו משפיע על קבלת לחיצות כפתורים. לחיצות על כפתורים מטוקנים נלכדות ללא תלות בערך expect — כל עוד הסשן חי וטוקן הכפתור חי. expect שולט רק בשאלה האם השירות מצפה לתשובה טקסטואלית/כלשהי.

גבולות דיאלוג

הקשר הדיאלוג חי בדיוק כל עוד הסשן פתוח:

  • תשובת טקסט תוך ציפייה פעילה (expect = text/any) — הולכת לשירות כאירוע message, ולא מפעילה ריאקציות בוט רגילות.
  • לחיצת כפתור מטוקן — הולכת לשירות כאירוע callback עם ה-callback_data המקורי. GetMyBot תמיד מאשר את הלחיצה בעצמו (answerCallbackQuery); נבדקת שייכות הלחיצה (בוט + משתמש + צ'אט סשן). לחיצה אינה סוגרת את הסשן.
  • /cancel (וגם /cancel@bot ו"ביטול") בסשן חיצוני פעיל — הסשן נסגר, session.cancel נשלח לשירות, המשתמש מקבל "בוטל.".
  • דיאלוג אחד למשתמש. פתיחת סשן חדש מחליפה את הסשן הפתוח הקיים של אותו משתמש — לו נשלח session.cancel.
  • תפוגת TTL. על פי טיימר הסשן מסומן כ-expired ו-(אם השירות מחובר) session.expired נשלח לשירות.

מגבלות וזמן קצוב

  • גודל frame: 256 KiB.
  • TTL סשן: ברירת מחדל 3600 שניות (ניתן להגדרה בפרמטר session_ttl_seconds של האינטגרציה).
  • תדירות פקודות: 60 execute לדקה לסשן (חלון נגלל).
  • Methods ב-execute אחד: עד 30 (עודפים נדחים).
  • סשנים פתוחים לאינטגרציה: עד 1000.
  • תור אופליין: עד 100 אירועים לסשן.
  • Heartbeat: ping כל 30 שניות; זמן קצוב לחוסר פעילות — 60 שניות.
  • זמן קצוב ל-auth frame: 10 שניות.

אמינות (heartbeat, אופליין, תור, fallback)

  • Heartbeat. השרת שולח frame {"type":"ping"} כל 30 שניות. אם לא הגיעה פעילות מהלקוח יותר מ-60 שניות — החיבור נסגר. הלקוח יכול לשלוח ping/pong משלו לשמירת פעילות. ניתוק אינו הורג דיאלוגים: הסשנים חיים ב-DB עד TTL או עד חיבור מחדש.
  • חיבור מחדש — בצד השירות. אם החיבור התנתק, השירות שלכם חייב להתחבר מחדש. סשנים פתוחים ב-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 הנשלט מבחוץ שהפלטפורמה פונה אליו.
  • הטוקן נשמר כ-hash (sha256), אימות בזמן קבוע (constant-time), נתמכת רוטציה.
  • בידוד דייר. פקודה קשורה בקפידה לאינטגרציה שלה; סשן — לבוט + משתמש + צ'אט. אירועי בוטים אחרים לא מגיעים לסוקט שלכם.
  • Whitelist methods + 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())

כפתור inline ועיבוד 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: "הכפתור נלחץ!" } }],
  }));
}

המשך מכאן