تطوير البوتات
Webhooks
كيف تجعل الخادم يرسل التحديثات الجديدة مباشرة إلى عنوان HTTPS الخاص ببوتك بدلاً من getUpdates — الإعداد والتحقق وإعادة المحاولة والرد من خلال الاستجابة.
getUpdates طريقة يسأل فيها البوت الخادم باستمرار. أما webhook فعلى العكس: عندما يظهر جديد يرسله خادم Pabal مباشرةً إلى عنوان HTTPS الخاص بالبوت. وإذا كان بوتك يعمل على خادم يبقى قيد التشغيل دائماً (خادم سحابي أو خادم داخلي في المؤسسة) فإن webhook هو الأنسب.
| getUpdates (الاستطلاع الطويل) | Webhook | |
|---|---|---|
| من يبدأ | البوت يسأل الخادم | الخادم يرسل إلى البوت |
| ما يحتاج إليه البوت | اتصال صادر بالإنترنت فقط | عنوان HTTPS عام |
| الأنسب لـ | حاسوبك الشخصي، وأثناء التطوير، وخلف جدار الحماية | خادم يعمل دائماً، والبيئات بلا خادم (serverless)، والبوتات المتعددة |
| الاستخدام معاً | غير ممكن — إذا كان webhook مضبوطاً فإن getUpdates يعيد 409 Conflict | |
كيف يسير webhook
شرح المخطط
- خطّان: على اليسار خادم 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 ثم عالجها في الخلفية.
الإعداد
- شغّل البرنامج الذي سيستلم webhook. لنفترض أنك شغّلت أحد الأمثلة أدناه على خادم البوت على
127.0.0.1:8081. - ضع HTTPS في الواجهة. مع Caddy مثلاً، يكفي هذان السطران لإعداد كل شيء بما في ذلك الشهادة تلقائياً.
bot.example.com { reverse_proxy 127.0.0.1:8081 } - أنشئ رمزاً سرياً (secret token). من 1 إلى 256 حرفاً من الأحرف اللاتينية والأرقام و
_و-. يضع الخادم هذه القيمة في ترويسة كل طلب، فتستطيع التحقق من أن الطلب جاء فعلاً من خادم Pabal.export WEBHOOK_SECRET=$(openssl rand -hex 32) - استدعِ 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} - تحقق من الحالة. أرسل رسالة إلى البوت من التطبيق، ثم اطّلع على
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
| المعامل | النوع | إلزامي | الوصف |
|---|---|---|---|
url | String | نعم | عنوان webhook. إذا كان سلسلة فارغة يُحذف webhook (مثل deleteWebhook). |
secret_token | String | اختياري | من 1 إلى 256 حرفاً، A-Z a-z 0-9 _ -. يُرسل مع كل طلب في الترويسة X-Telegram-Bot-Api-Secret-Token. |
allowed_updates | Array of String | اختياري | الأنواع المراد استلامها: message، edited_message، callback_query. إذا تُركت فارغة فكلها. والأنواع غير المدرجة لا تُرسل بل تُهمل. |
drop_pending_updates | Boolean | اختياري | إذا كانت true تُهمل كل التحديثات التي لم تُسلَّم بعد قبل البدء. |
max_connections | Integer | اختياري | من 1 إلى 100، والافتراضي 40. تُقبل القيمة وتُحفظ، لكن Pabal يرسل تحديثاً واحداً في كل مرة لكل بوت حفاظاً على الترتيب. |
certificate | InputFile | غير مدعوم | لا تُقبل الشهادات الموقّعة ذاتياً (400). استخدم شهادة عامة. |
ip_address | String | يُتجاهل | يُقبل لكنه لا يُستخدم. يستعلم الخادم عن النطاق في كل مرة. |
يُحفظ إعداد 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من جديد بالرمز الجديد. وغيّر الرمز السري معه أيضاً.