وثائق المطوّرين
العربية

تطوير البوتات

Webhooks

كيف تجعل الخادم يرسل التحديثات الجديدة مباشرة إلى عنوان HTTPS الخاص ببوتك بدلاً من getUpdates — الإعداد والتحقق وإعادة المحاولة والرد من خلال الاستجابة.

getUpdates طريقة يسأل فيها البوت الخادم باستمرار. أما webhook فعلى العكس: عندما يظهر جديد يرسله خادم Pabal مباشرةً إلى عنوان HTTPS الخاص بالبوت. وإذا كان بوتك يعمل على خادم يبقى قيد التشغيل دائماً (خادم سحابي أو خادم داخلي في المؤسسة) فإن webhook هو الأنسب.

getUpdates (الاستطلاع الطويل)Webhook
من يبدأالبوت يسأل الخادمالخادم يرسل إلى البوت
ما يحتاج إليه البوتاتصال صادر بالإنترنت فقطعنوان HTTPS عام
الأنسب لـحاسوبك الشخصي، وأثناء التطوير، وخلف جدار الحمايةخادم يعمل دائماً، والبيئات بلا خادم (serverless)، والبوتات المتعددة
الاستخدام معاًغير ممكن — إذا كان webhook مضبوطاً فإن getUpdates يعيد 409 Conflict

كيف يسير webhook

⁧خادم Pabal⁩ ⁧webhook البوت (HTTPS)⁩ ⁧① POST التحديث 5 · X-Telegram-Bot-Api-Secret-Token⁩ ⁧② 200 OK ← تأكيد 5، ثم التالي⁩ ⁧③ POST التحديث 6⁩ ⁧④ 500 · فشل الاتصال · تجاوز 30 ثانية ← تسجيل last_error⁩ ⁧بفواصل 1 ثانية ← 2 ← 4 … حتى 60 ثانية⁩ ⁧يُعاد إرسال 6 نفسه (وينتظر 7 خلف 6)⁩ ⁧⑤ POST التحديث 6 (مجدداً)⁩ ⑥ 200 + {"method": "sendMessage", "chat_id": …, "text": …} تنفيذ الطريقة الواردة في الاستجابة نيابةً عن البوت ⁧← يصل الرد إلى تطبيق الشخص⁩ ⁧⑦ POST التحديث 7 …⁩
تُرسل تحديثات البوت الواحد واحداً تلو الآخر، بالترتيب

شرح المخطط

  • خطّان: على اليسار خادم Pabal، وعلى اليمين عنوان webhook الذي تشغّله أنت. الخطوط الزرقاء المتصلة طلبات يرسلها الخادم، والرمادية الاستجابات العادية من webhook، والحمراء المتقطعة حالات الفشل.
  • الاستجابة 2xx تعني «تم الاستلام» (②). وحتى ذلك الحين يبقى التحديث على الخادم. ويجوز أن يكون جسم الاستجابة فارغاً.
  • عند الفشل يُعاد إرسال التحديث نفسه (④ ← ⑤): يُعدّ فشلاً كلٌّ من الاستجابة التي ليست 2xx، وفشل الاتصال، وعدم الرد خلال 30 ثانية. وتبدأ الفترة من ثانية واحدة وتتضاعف حتى 60 ثانية كحد أقصى. ويُسجَّل سبب الفشل ووقته في getWebhookInfo.
  • يُحافَظ على الترتيب: تُرسل تحديثات البوت الواحد واحداً تلو الآخر، لذا ينتظر 7 حتى ينجح 6. ولهذا إذا فشل webhook مدة طويلة تتراكم التحديثات اللاحقة (pending_update_count).
  • يمكنك الرد من خلال الاستجابة (⑥): إذا وضعت في جسم الاستجابة 200 الحقل method مع المعاملات، ينفّذ الخادم تلك الطريقة نيابةً عن البوت. وهذا أسرع لأنك لا تحتاج إلى إرسال طلب إضافي.

شروط عنوان webhook

  • يجب أن يكون عنوان https://. ويجب أن تكون الشهادة صادرة عن جهة تصديق عامة (مثل Let's Encrypt)، أما رفع الشهادات الموقّعة ذاتياً (certificate) فغير مدعوم.
  • يجب أن يكون عنواناً عاماً يمكن الوصول إليه من الإنترنت. لا يرسل الخادم إلى العناوين الداخلية مثل عنوان الاسترجاع (127.0.0.1، ::1)، والشبكات الخاصة (10.، 172.16–31.، 192.168.)، وعناوين الربط المحلي (169.254.، والبيانات الوصفية السحابية)، وCGNAT (100.64/10). ويرفض كذلك النطاق الذي يشير إلى مثل هذه العناوين، ويعيد التحقق عند كل إرسال.
  • لا قيود على المنفذ (لا يلزم أن يكون 443). ولا يمكن وضع اسم مستخدم وكلمة مرور في العنوان (https://user:pw@…).
  • لا تُتبع عمليات إعادة التوجيه (3xx)، بل تُعدّ فشلاً. ضع العنوان النهائي.
  • يجب الرد خلال 30 ثانية. أما الأعمال التي تستغرق وقتاً طويلاً فردّ عليها أولاً بـ 200 ثم عالجها في الخلفية.

الإعداد

  1. شغّل البرنامج الذي سيستلم webhook. لنفترض أنك شغّلت أحد الأمثلة أدناه على خادم البوت على 127.0.0.1:8081.
  2. ضع HTTPS في الواجهة. مع Caddy مثلاً، يكفي هذان السطران لإعداد كل شيء بما في ذلك الشهادة تلقائياً.
    bot.example.com {
        reverse_proxy 127.0.0.1:8081
    }
  3. أنشئ رمزاً سرياً (secret token). من 1 إلى 256 حرفاً من الأحرف اللاتينية والأرقام و_ و-. يضع الخادم هذه القيمة في ترويسة كل طلب، فتستطيع التحقق من أن الطلب جاء فعلاً من خادم Pabal.
    export WEBHOOK_SECRET=$(openssl rand -hex 32)
  4. استدعِ setWebhook.
    curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' -d "{
      \"url\": \"https://bot.example.com/pabal-webhook\",
      \"secret_token\": \"$WEBHOOK_SECRET\",
      \"allowed_updates\": [\"message\", \"callback_query\"],
      \"drop_pending_updates\": true
    }"
    # {"ok":true,"result":true}
  5. تحقق من الحالة. أرسل رسالة إلى البوت من التطبيق، ثم اطّلع على getWebhookInfo.
    curl -s "https://pabal.me/bot$BOT_TOKEN/getWebhookInfo"
    {
      "ok": true,
      "result": {
        "url": "https://bot.example.com/pabal-webhook",
        "has_custom_certificate": false,
        "pending_update_count": 0,
        "ip_address": "198.51.100.7",
        "max_connections": 40,
        "allowed_updates": ["message", "callback_query"]
      }
    }

    إذا كانت pending_update_count تساوي 0 فالاستلام يعمل جيداً. أما last_error_date وlast_error_message فهما سجل آخر فشل، ويبقيان حتى بعد التعافي — فاحكم بالنظر إلى الوقت.

معاملات setWebhook

المعاملالنوعإلزاميالوصف
urlStringنعمعنوان webhook. إذا كان سلسلة فارغة يُحذف webhook (مثل deleteWebhook).
secret_tokenStringاختياريمن 1 إلى 256 حرفاً، A-Z a-z 0-9 _ -. يُرسل مع كل طلب في الترويسة X-Telegram-Bot-Api-Secret-Token.
allowed_updatesArray of Stringاختياريالأنواع المراد استلامها: message، edited_message، callback_query. إذا تُركت فارغة فكلها. والأنواع غير المدرجة لا تُرسل بل تُهمل.
drop_pending_updatesBooleanاختياريإذا كانت true تُهمل كل التحديثات التي لم تُسلَّم بعد قبل البدء.
max_connectionsIntegerاختياريمن 1 إلى 100، والافتراضي 40. تُقبل القيمة وتُحفظ، لكن Pabal يرسل تحديثاً واحداً في كل مرة لكل بوت حفاظاً على الترتيب.
certificateInputFileغير مدعوملا تُقبل الشهادات الموقّعة ذاتياً (400). استخدم شهادة عامة.
ip_addressStringيُتجاهليُقبل لكنه لا يُستخدم. يستعلم الخادم عن النطاق في كل مرة.

يُحفظ إعداد webhook على الخادم، فـيبقى حتى بعد إعادة تشغيل الخادم، ويُستأنف الإرسال فور التشغيل. لكن التحديثات التي لم تُسلَّم بعد موجودة في الذاكرة، فتضيع عند إعادة التشغيل.

الطلب الذي يرسله الخادم

POST /pabal-webhook HTTP/1.1
Host: bot.example.com
Content-Type: application/json
X-Telegram-Bot-Api-Secret-Token: 3f1c…(القيمة التي مرّرتها إلى setWebhook)

{"update_id":12,"message":{"message_id":3,"from":{"id":100001,"is_bot":false,"first_name":"هناء"},"chat":{"id":100001,"first_name":"هناء","type":"private"},"date":1789805661,"text":"مرحباً"}}
  • الجسم كائن Update مطابق تماماً لعنصر واحد من نتيجة getUpdates.
  • تحقق دائماً من الرمز السري. إذا كانت الترويسة غائبة أو مختلفة فارفض الطلب بـ 401. وأجرِ المقارنة بدالة ثابتة الزمن (hmac.compare_digest، crypto.timingSafeEqual).
  • قد يصل التحديث نفسه مرتين (إذا انقطع الاتصال قبل أن تصل استجابتك إلى الخادم). ومن الآمن أن تتخطى ما عالجته من قبل بالاعتماد على update_id.

الرد من خلال الاستجابة

إذا وضعت استدعاءً واحداً لـ Bot API في جسم الاستجابة 200، ينفّذ الخادم ذلك الاستدعاء نيابةً عن البوت. ضع اسم الطريقة في method وأضف معه بقية المعاملات.

{"method": "sendMessage", "chat_id": 100001, "text": "أهلاً بك!"}
  • يمكن أن يكون الجسم بصيغة JSON أو نموذج أو multipart/form-data (ترسل aiogram بصيغة multipart). والحد الأقصى للحجم 1MB.
  • نتيجة هذا الاستدعاء أو خطؤه لا تعود إلى البوت (تُسجَّل في سجل الخادم فقط). وإذا احتجت إلى النتيجة فأرسل طلباً منفصلاً كالمعتاد.
  • إذا لم يكن لديك ما ترد به، فيكفي أن تعيد 200 دون جسم.

أمثلة

الأمثلة الأربعة كلها تتحقق من الرمز السري، وعندما تستلم رسالة نصية تضع sendMessage في الاستجابة لتكرّر النص. ونفترض أن HTTPS (مثل Caddy) موضوع في الواجهة، وأن الاستلام يتم على 127.0.0.1:8081.

# webhook.py — المكتبة القياسية فقط
# التشغيل: WEBHOOK_SECRET='…' python3 webhook.py
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["WEBHOOK_SECRET"]


class Webhook(BaseHTTPRequestHandler):
    def do_POST(self):
        got = self.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
        if not hmac.compare_digest(got, SECRET):
            self.send_response(401)
            self.end_headers()
            return
        update = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        message = update.get("message")
        answer = b""
        if message and "text" in message:
            answer = json.dumps({"method": "sendMessage",
                                 "chat_id": message["chat"]["id"],
                                 "text": message["text"]}).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(answer)))
        self.end_headers()
        self.wfile.write(answer)


HTTPServer(("127.0.0.1", 8081), Webhook).serve_forever()
// webhook.mjs — Node.js 18 أو أحدث، دون مكتبة
// التشغيل: WEBHOOK_SECRET='…' node webhook.mjs
import http from 'node:http';
import { timingSafeEqual } from 'node:crypto';

const SECRET = Buffer.from(process.env.WEBHOOK_SECRET);

http.createServer(async (req, res) => {
  const got = Buffer.from(req.headers['x-telegram-bot-api-secret-token'] ?? '');
  if (req.method !== 'POST' || got.length !== SECRET.length || !timingSafeEqual(got, SECRET)) {
    res.writeHead(401).end();
    return;
  }
  let body = '';
  for await (const chunk of req) body += chunk;
  const message = JSON.parse(body).message;
  if (message?.text) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ method: 'sendMessage', chat_id: message.chat.id, text: message.text }));
  } else {
    res.writeHead(200).end();
  }
}).listen(8081, '127.0.0.1');
# webhook_aiogram.py — pip install aiogram
# يتولى هذا البرنامج أيضاً إعداد webhook (setWebhook)
import os

from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web

URL = "https://bot.example.com/pabal-webhook"
SECRET = os.environ["WEBHOOK_SECRET"]
dp = Dispatcher()


@dp.message()
async def echo(message):
    if message.text:
        return message.answer(message.text)       # return: يُرسل ضمن استجابة webhook


async def on_startup(bot: Bot):
    await bot.set_webhook(URL, secret_token=SECRET)


def main():
    session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))
    bot = Bot(os.environ["BOT_TOKEN"], session=session)
    dp.startup.register(on_startup)
    app = web.Application()
    SimpleRequestHandler(dispatcher=dp, bot=bot, secret_token=SECRET).register(app, path="/pabal-webhook")
    setup_application(app, dp, bot=bot)
    web.run_app(app, host="127.0.0.1", port=8081)


main()
# webhook_ptb.py — pip install "python-telegram-bot[webhooks]"
# يتولى هذا البرنامج أيضاً إعداد webhook (setWebhook)
import os

from telegram import Update
from telegram.ext import Application, ContextTypes, MessageHandler, filters

SERVER = "https://pabal.me"


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(update.message.text)


app = (Application.builder().token(os.environ["BOT_TOKEN"])
       .base_url(f"{SERVER}/bot").base_file_url(f"{SERVER}/file/bot").build())
app.add_handler(MessageHandler(filters.TEXT, echo))
app.run_webhook(listen="127.0.0.1", port=8081, url_path="pabal-webhook",
                webhook_url="https://bot.example.com/pabal-webhook",
                secret_token=os.environ["WEBHOOK_SECRET"])

عرض الحالة — getWebhookInfo

الحقلالمعنى
urlالعنوان المضبوط. سلسلة فارغة إذا لم يكن هناك webhook
pending_update_countعدد التحديثات التي لم تُسلَّم بعد وهي في الانتظار
ip_addressعنوان IP للوجهة التي أُرسل إليها آخر مرة
last_error_date، last_error_messageوقت آخر فشل (بثواني يونكس) وسببه. لا يظهران إن لم يحدث فشل قط، ويبقيان حتى بعد التعافي
max_connections، allowed_updatesالقيم التي مرّرتها إلى setWebhook
has_custom_certificateدائماً false

الرسائل التي تظهر في last_error_message

الرسالةالسبب
Connection refusedلا شيء يستمع على ذلك العنوان والمنفذ. برنامج webhook أو الوكيل (proxy) متوقف
Connection timed outجدار حماية يحجب الاتصال، أو يتعذّر الوصول إلى العنوان
Read timeout expiredلم يصل رد خلال 30 ثانية
Failed to resolve host: Name or service not knownتعذّر العثور على اسم النطاق (DNS)
SSL error {…}مشكلة في الشهادة — منتهية الصلاحية، أو موقّعة ذاتياً، أو الاسم غير مطابق
Wrong response from the webhook: 502 Bad Gatewayاستجابة ليست 2xx. والرقم هو رمز الحالة الذي أعاده webhook
IP address 10.0.0.5 is reservedالنطاق يشير إلى عنوان داخلي (فلا يُرسل إليه شيء)

العودة إلى getUpdates

curl -s "https://pabal.me/bot$BOT_TOKEN/deleteWebhook"
# لإهمال التحديثات المنتظرة أيضاً: deleteWebhook?drop_pending_updates=true

التحديثات التي استلمها webhook (أي التي ردّ عليها بـ 2xx) لا تصل مرة أخرى عبر getUpdates. وإنما تتابع عبر getUpdates استلام ما لم يُسلَّم بعد فقط.

الاختبار أثناء التطوير

  • النفق: إذا استخدمت أداة تربط عنواناً عاماً بـ HTTPS بالعنوان 127.0.0.1:8081 على حاسوبك (مثل cloudflared وngrok)، يمكنك الاختبار حتى على خادم Pabal الإنتاجي.
  • خادم Pabal الخاص بك: إذا شغّلت بنفسك خادم Pabal للتطوير، ففعّل TELEGRAM_WEBHOOK_ALLOW_LOCAL=true. عندها يرسل ذلك الخادم أيضاً إلى عناوين محلية مثل http://127.0.0.1:8081/…. لا تفعّله أبداً على خادم الإنتاج — لأنه يتيح لأي بوت أن يرسل طلبات إلى الشبكة الداخلية للخادم (قاعدة البيانات، والبيانات الوصفية السحابية).

قائمة التحقق للتشغيل

  • حدّدتَ رمزاً سرياً وتتحقق منه في كل طلب.
  • ترد خلال 30 ثانية — الأعمال الطويلة تُحال إلى قائمة انتظار ويُرد بـ 200 فوراً.
  • تستبعد التكرار بالاعتماد على update_id.
  • إذا توقف webhook مدة طويلة تتراكم التحديثات اللاحقة وتضيع عند إعادة تشغيل الخادم — فراقب برنامج webhook (pending_update_count وlast_error_date في getWebhookInfo).
  • إذا تسرّب الرمز المميّز فاستخدم /revoke، ثم استدعِ setWebhook من جديد بالرمز الجديد. وغيّر الرمز السري معه أيضاً.