المعالج الخارجي

المعالج الخارجي هو جسر بين كودك وTelegram. عندما لا تكفي منطق المنشئ البصري، تُسلّم تفاعلاً بأكمله لخادمك: يستقبل الأحداث ويردّ بأوامر بتنسيق Telegram Bot API، بينما يتولى GetMyBot النقل والتسليم وكل ما يخص Telegram.

ما هو

تُبنى التفاعلات في GetMyBot بصرياً: مُشغِّل ← شروط ← أفعال. المنطق المعقد أو غير المعياري — قاعدة البيانات الخاصة بك، الحسابات، الاتصالات بأنظمة خارجية، تعلم الآلة، السيناريوهات المتشعبة — يصعب التعبير عنها في المنشئ. يرفع المعالج الخارجي هذا السقف: تُوصّل الـ backend الخاص بك وتُسلّم إليه تفاعلات بأكملها.

يستقبل الخدمة الخارجية من GetMyBot أحداثاً (تفعّل مُشغِّل، المستخدم أرسل نصاً، ضغط زراً) ويصدر أوامر للبوت بما يُرسله. ينفّذ GetMyBot الأوامر بتوكن البوت الخاص به — مع تطبيق rate-limit والإعادة وإزالة التكرار وسجل المحادثة فوق ذلك.

نموذج العمل

  • GetMyBot هو WebSocket-سيرفر. خدمتك هي التي تتصل به ولا تحتاج عنواناً عاماً أو endpoint للـ webhook. GetMyBot لا يُنشئ اتصالات صادرة نحو خدمتك، لذا لا يوجد سطح SSRF.
  • المصادقة برمز التكامل. تُقدّم الخدمة الرمز عند الاتصال؛ يتحقق GetMyBot منه بالمقارنة مع الهاش ويُبقي الاتصال.
  • الأحداث والأوامر بتنسيق Telegram Bot API. تستقبل الخدمة حدثاً وتردّ بمصفوفة من الاستدعاءات بصيغة «طريقة + معاملات»، تماماً كما عند العمل مع بوت عادي.
  • الأوامر يُنفّذها GetMyBot بتوكنه. الخدمة لا تتواصل مع Telegram مباشرةً أبداً: تمر الأوامر عبر التحقق وتدخل outbox GetMyBot الداخلي، ثم تُرسَل بتوكن البوت مع rate-limit والإعادة وإزالة التكرار والسجل.

اتصال واحد يخدم جميع محادثات تكامل واحد — تتشابك أحداث المستخدمين المختلفين وتتمايز بالحقل session_id.

إعداد التكامل

  1. افتح قسم التكاملات في القائمة اليسرى وانقر إضافة تكامل.
  2. اختر خدمة المعالج الخارجي وحدّد اسماً.
  3. بعد الإنشاء ستُظهر البطاقة:
    • عنوان WebSocket — بالشكل wss://api.mybot.app/ext/ws/<integrationID>، حيث <integrationID> هو UUID هذا التكامل. به تتصل خدمتك. بدون المعرّف في المسار لن يتم الاتصال.
    • رمز الاتصال — يظهر مرة واحدة عند الإنشاء. انسخه واحفظه في مكان آمن؛ لا يمكن الاطلاع على الرمز مجدداً (مخزون فقط بالهاش).
    • إعادة إصدار الرمز — الزر يُنشئ رمزاً جديداً ويُلغي القديم فوراً. بعد إعادة الإصدار حدّث الرمز في خدمتك.
    • مؤشر الاتصال — يُظهر هل تحتفظ خدمتك بالاتصال النشط الآن.

معاملات تكامل إضافية تؤثر على البروتوكول:

  • session_ttl_seconds — مدة بقاء الجلسة بالثواني (الافتراضي 3600).
  • on_unavailable_reaction_id — التفاعل الاحتياطي الذي يُشغَّل إذا كانت الخدمة غير متصلة وقت التفعيل (انظر الموثوقية).

الربط بالتفاعل

لا يتصل المعالج الخارجي عبر مُشغِّل منفصل، بل كفعل. في محرر التفاعل أضف فعل التسليم للمعالج الخارجي واختر التكامل المطلوب.

يمكن لأي مُشغِّل موجود في GetMyBot فتح المحادثة — أمر، نص، نقرة زر، معامل من رابط، طلب ويب وارد، جدول. عندما يتفعّل هذا المُشغِّل ويصل إلى الفعل، يفتح GetMyBot جلسة وكيل ويرسل للخدمة حدث session.open.

إذا لم تكن الخدمة متصلة في تلك اللحظة، سيُشغّل GetMyBot التفاعل الاحتياطي من إعداد التكامل (on_unavailable_reaction_id). إذا لم يُحدَّد احتياطي — لن يحدث شيء (صامت، دون خطأ للمستخدم).

الاتصال والمصادقة

الـ endpoint: GET /ext/ws/{integrationID} فوق WebSocket (wss). integrationID هو UUID للتكامل من نوع «المعالج الخارجي». حد حجم الإطار — 256 KiB.

يمكن المصادقة بإحدى طريقتين.

1. رأس عند المصافحة (مُفضَّل). مرّر الرمز في رأس Authorization:

Authorization: Bearer <token>

2. إطار auth. إذا لم يُمرَّر رأس Bearer، أرسل كأول إطار خلال 10 ثوانٍ من إنشاء الاتصال:

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

إذا لم يصل إطار auth خلال 10 ثوانٍ — يُغلق الاتصال بالكود 4401. الرمز الخاطئ في أي من الطريقتين — أيضاً close 4401.

يُنشأ الرمز عند إنشاء التكامل، يظهر مرة واحدة ومخزون فقط بالهاش (sha256)، والمقارنة constant-time. تدوير الرمز:

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

يُعيد الـ endpoint الرمز الجديد بنص واضح؛ القديم يتوقف عن العمل فوراً.

الأحداث: البوت ← الخدمة

كل حدث إطار 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": { "...": "raw Telegram Update (raw JSON)" },
  "event": { "...": "normalized GetMyBot event (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 (raw 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 (مثلاً sendMessageparams — كائن معاملات الطريقة.
  • expect — هل تنتظر الخدمة ردّ المستخدم: "none"، "text" أو "any" (القيمة الفارغة تُعامَل كـ none).
  • session.close — إغلاق المحادثة بشكل صريح.

الأوامر لا تذهب مباشرة إلى Telegram: يتحقق منها GetMyBot ويضعها في outbox الخاص به، ومنه يُرسلها بتوكن البوت (مع rate-limit والإعادة وإزالة التكرار والسجل).

كيف تُعالَج execute

يُسقَط أمر execute بأكمله إذا: session_id فارغ؛ الجلسة غير موجودة أو ليست بحالة open؛ الجلسة تنتمي لتكامل آخر؛ الجلسة منتهية الصلاحية.

ثم:

  • Rate-limit: حتى 60 execute في الدقيقة لكل جلسة (نافذة متحركة). ما فوق الحد — يُسقَط.
  • حتى 30 طريقة في execute واحد؛ الزائدة تُتجاهل بصمت.
  • لكل طريقة يُتحقق من القائمة البيضاء (انظر الطرق المسموح بها) — الطريقة غير المدرجة تُرفض.
  • حارس المحادثة (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 مع callback_data الأصلي. 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: ping كل 30 ثانية؛ مهلة غياب النشاط — 60 ثانية.
  • مهلة إطار auth: 10 ثوانٍ.

الموثوقية (Heartbeat، غير متصل، القائمة، والـ fallback)

  • Heartbeat. يُرسل السيرفر إطار {"type":"ping"} كل 30 ثانية. إذا لم يكن هناك نشاط من العميل أكثر من 60 ثانية — يُغلق الاتصال. يمكن للعميل إرسال ping/pong للحفاظ على النشاط. قطع الاتصال لا يُنهي المحادثات: تعيش الجلسات في قاعدة البيانات حتى TTL أو إعادة الاتصال.
  • إعادة الاتصال على جانب الخدمة. إذا انقطع الاتصال، خدمتك يجب أن تتصل من جديد. الجلسات المفتوحة في قاعدة البيانات تنجو من إعادة التشغيل حتى انتهاء TTL.
  • Fallback عند الغياب عند البدء. إذا لم تكن الخدمة متصلة وقت تفعيل التفاعل — يُشغَّل التفاعل الاحتياطي من إعداد التكامل (on_unavailable_reaction_id). إذا لم يُحدَّد — لا يحدث شيء (صامت).
  • قائمة انتظار الغير متصل. أحداث message/callback غير المُسلَّمة بسبب الانقطاع تُخزَّن في قائمة انتظار الغير متصل (حتى 100 حدث، FIFO، تُحفظ حتى انتهاء الجلسة) وتُرسَل عند إعادة الاتصال. أحداث session.open/session.cancel/session.expired لا تُوضع في القائمة.
  • ضمان التسليم — at-most-once. عند امتلاء القائمة أو انتهاء صلاحيتها تُسقَط الأحداث. صمّم منطقك بحيث لا يُفسد فقدان حدث واحد السيناريو.

مؤشر الاتصال

للتحقق مما إذا كانت الخدمة تحتفظ بالاتصال النشط، استخدم الـ endpoint:

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

الرد:

{ "online": true }

نفس المؤشر متاح على بطاقة التكامل في لوحة التحكم. المكافئ في MCP هو أداة get_integration_status.

الأمان

ينفّذ GetMyBot أوامر الخدمة الخارجية بتوكنه الخاص، لذا الحماية صارمة.

  • لا يوجد سطح SSRF. GetMyBot يعمل كـ WebSocket-سيرفر ولا يُنشئ اتصالات صادرة للخدمة — الخدمة هي التي تتصل بـ GetMyBot. لا يوجد URL يتحكم به الخارج تذهب إليه المنصة.
  • الرمز مخزون بالهاش (sha256)، المقارنة constant-time، التدوير مدعوم.
  • عزل المستأجر. الأمر مقيّد بتكامله بشكل صارم؛ الجلسة مقيّدة بالبوت + المستخدم + المحادثة. أحداث البوتات الأخرى لا تصل إلى Socket الخاص بك.
  • القائمة البيضاء للطرق + إلزامية chat_id لا تسمح بتحويل البوت لأداة إرسال لمحادثات عشوائية.
  • توكن البوت لا يُرسَل للخارج — الخدمة لا تتواصل مع Telegram مباشرةً أبداً.
  • إخفاء الأسرار. رمز التكامل والقيم الحساسة مخفية في السجلات واللوجات.

أمثلة

معالج أدنى: عند فتح المحادثة يرسل رسالة ويغلق الجلسة (expect: "none"). ضع معرّف التكامل والرمز. يجب أن يحتوي العنوان <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: "تم الضغط على الزر!" } }],
  }));
}

ما التالي