الرسائل عبر Telegram
خادم اللوحة في الفصل 14 لا يُرى إلا من داخل الشبكة المنزلية. فكيف نعرف أن الباب فُتح ونحن في الخارج؟ الحل هو أن نترك اللوحة تكلّم خادما عامّا موجودا دائما. في هذا الفصل نبني بوتا على Telegram: اللوحة ترسل إلى هاتفنا رسالة، وتنفّذ الأوامر التي نكتبها لها، وتحمي الرمز الذي يفتح كل ذلك.
- ننشئ بوتا عند BotFather ونفهم ما هو الرمز ومعرّف المحادثة.
- نحفظ الرمز في
secrets.py، ونعرف ما الذي نفعله إذا تسرّب. - نرسل رسالة بالدالة
requests.postوالأمرsendMessage. - نستقبل الأوامر بالأمر
getUpdates، ونفهم دورoffset. - نبني بوتا يعيد قراءة حسّاس ويشغّل ثنائيا باعثا للضوء، ولا يطيع إلا صاحبه.
- نحسب عدد الطلبات، ونختار زمن استطلاع معقولا.
18.1 البوت: حساب تقوده اللوحة – בוט ויצירתו (Bots and BotFather)
البوت (בוט) هو حساب في Telegram يكتب فيه برنامج بدلا من إنسان. ولأن اللوحة هي التي تبدأ كل اتصال، فلا نحتاج إلى عنوان عام ولا إلى تغيير في الموجّه: الطلب الخارج يمر كما يمر طلب الهاتف.
ننشئ البوت من الهاتف، دون أي برمجة:
- نفتح في Telegram المحادثة مع @BotFather، وهو البوت الرسمي لإدارة البوتات.
- نرسل الأمر
/newbot. يسألنا عن اسم للعرض، ثم عن اسم مستخدم يجب أن ينتهي بـbot. - يردّ علينا بالرمز. ننسخه فورا إلى
secrets.py، ولا نلصقه في أي مكان آخر. - نفتح المحادثة مع البوت الجديد ونضغط Start، ثم نرسل له أي رسالة.
chat_id. نعرفه من الرسالة التي أرسلناها إلى البوت، كما نرى في البند 18.4.123:TEST ومعرّف المحادثة 555. والخادم مُحاكى أيضا: الردود مكتوبة في عالم المحاكاة، ولم يُرسل أي طلب إلى Telegram عند بناء الكتاب.18.2 الرمز كلمة مرور – הטוקן הוא סיסמה
من يملك الرمز يملك البوت: يقرأ ما يصله من رسائل، ويرسل باسمه، ويأمر لوحتنا بما يشاء. لا يوجد اسم مستخدم ولا سؤال إضافي، فالرمز وحده يكفي. لذلك نعامله كما نعامل كلمة مرور WiFi: مكانه الوحيد سطر في secrets.py.
secrets.py على اللوحة، بقيمة مصطنعة. لا يعمل هذا الملف وحده، ولذلك لم يُشغَّل عند بناء الكتاب.WIFI_SSID = "my-network" WIFI_PASS = "my-password" TELEGRAM_TOKEN = "123:TEST"
- البرنامج يستورد الاسم فقط:
from secrets import TELEGRAM_TOKEN. فنستطيع مشاركة البرنامج كله دون أن نشارك الرمز. - الملف
secrets.pyلا يذهب إلى GitHub ولا إلى مجلد مشترك ولا إلى صورة شاشة ترسل إلى صديق. - الرمز جزء من عنوان الطلب. فإذا طبعنا العنوان للفحص، صار الرمز مكتوبا في السجل. لذلك لا نطبع
APIأبدا.
ماذا نفعل إذا تسرّب الرمز؟ بهذا الترتيب:
- نلغي الرمز فورا عند BotFather: نرسل الأمر
/tokenونختار البوت، فيعطينا رمزا جديدا. وتوثيق Telegram يذكر هذا الأمر للرمز المتسرّب، وفي قائمة أوامر BotFather نجد أيضا/revoke. - نضع الرمز الجديد في
secrets.pyعلى كل لوحة تستعمله. - نفحص أن الرمز القديم لم يعد يعمل: طلب به يجب أن يُرفض.
- نبحث عن مصدر التسرّب ونغلقه. وإذا كان الرمز قد دخل مستودع شيفرة، فهو باقٍ في تاريخه حتى بعد حذف السطر.
18.3 إرسال رسالة – שליחת הודעה (sendMessage)
كل أوامر البوت طلبات HTTPS إلى عنوان واحد الشكل: https://api.telegram.org/bot<TOKEN>/METHOD. والرد نص JSON فيه الحقل ok، ومعه result عند النجاح أو description عند الفشل.
import time
import network
import requests
from secrets import WIFI_SSID, WIFI_PASS, TELEGRAM_TOKEN
CHAT = 555
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
time.sleep(0.5)
r = requests.post(API + "/sendMessage",
json={"chat_id": CHAT, "text": "board is up"})
reply = r.json()
if reply["ok"]:
print(r.status_code, reply["ok"], reply["result"]["message_id"])
else:
print(r.status_code, reply["description"])
r.close()200 True 7
[http_sent]
POST https://api.telegram.org/bot123:TEST/sendMessage {'text': 'board is up', 'chat_id': 555}نرسل البيانات بالمعامل json=، فتتولى المكتبة تحويلها إلى نص JSON، ولا نحتاج إلى ترميز المسافات في العنوان. ولا ننسى r.close(): من دونه يبقى الاتصال مفتوحا والذاكرة محجوزة.
والآن نرى ما يحدث بعد إلغاء الرمز. الخادم يردّ بالرمز 401، فيعرف البرنامج أن الرمز غير صالح:
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
bot = Bot(TELEGRAM_TOKEN)
code = bot.send(555, "still working?")
print("status", code)
if code == 401:
print("token rejected: put the new one in secrets.py")status 401 token rejected: put the new one in secrets.py
requests في مايكروبايثون لا تتحقق من شهادة الخادم في إعدادها العادي، فهي لا تضمن أننا نكلّم Telegram فعلا. وهذا مقبول في تجربة تعليمية، لكنه ليس كذلك في نظام حرج.tg_check_token.py لو كان الرمز صالحا وردّ الخادم بالرمز 200؟18.4 استقبال الأوامر – קבלת פקודות (getUpdates)
الخادم لا يستطيع الاتصال باللوحة، فهو يحتفظ بكل رسالة حتى تسأل عنها. كل رسالة تصل هي تحديث (עדכון) له رقم update_id، والسؤال المتكرر اسمه الاستطلاع (תשאול). لنكتب أبسط برنامج ممكن:
import time
import requests
from secrets import TELEGRAM_TOKEN
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
for i in range(3):
r = requests.get(API + "/getUpdates")
data = r.json()
r.close()
if not data["ok"]:
print(data["description"])
break
for u in data["result"]:
print("poll", i, u["update_id"], u["message"]["text"])
time.sleep(5)poll 0 100 /on poll 1 100 /on poll 2 100 /on
السبب أن الخادم لا يعرف أننا عالجنا الرسالة. نخبره بذلك بالمعامل offset:
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| updateid | رقم آخر تحديث عالجناه | — |
| offset | أصغر رقم نريد رؤيته في الطلب التالي | — |
import time
import requests
from secrets import TELEGRAM_TOKEN
API = "https://api.telegram.org/bot" + TELEGRAM_TOKEN
offset = 0
for i in range(3):
r = requests.get("%s/getUpdates?offset=%d" % (API, offset))
data = r.json()
r.close()
if not data["ok"]:
print(data["description"])
break
print("poll", i, "new:", len(data["result"]))
for u in data["result"]:
offset = u["update_id"] + 1
m = u["message"]
print(" ", m["chat"]["id"], m["text"])
time.sleep(5)poll 0 new: 2 555 /on 555 /light poll 1 new: 1 555 /off poll 2 new: 0
getUpdates بـoffset أكبر من رقمه، والتحديث غير المؤكّد لا يبقى على الخادم أكثر من 24 ساعة. فإذا فُصلت اللوحة يوما ونصف اليوم، ضاعت أوامر اليوم الأول.offset في الطلب التالي؟ وماذا يحدث لو أرسلنا 240؟18.5 بوت يقرأ حسّاسا ويشغّل ثنائيا – בוט שמחזיר קריאת חיישן ומפעיל נורית
نجمع الطلبات في مكتبة صغيرة، فيبقى البرنامج الرئيسي واضحا. وهي تأخذ الرمز من البرنامج، ولا تعرف قيمته:
tgbot.py.# tgbot.py - a small Telegram bot client over HTTPS.
# The token comes from secrets.py. Never print self.api: it holds the token.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time
import requests
class Bot:
def __init__(self, token):
self.api = "https://api.telegram.org/bot" + token
self.offset = 0
def send(self, chat, text):
r = requests.post(self.api + "/sendMessage",
json={"chat_id": chat, "text": text})
code = r.status_code
r.close()
return code
def updates(self):
url = "%s/getUpdates?offset=%d" % (self.api, self.offset)
r = requests.get(url)
code = r.status_code
data = r.json()
r.close()
if code != 200:
raise OSError(code)
found = []
for u in data["result"]:
self.offset = u["update_id"] + 1
m = u.get("message")
if m and "text" in m:
found.append((m["chat"]["id"], m["text"]))
return foundimport time
from machine import Pin
import board
from convert import to_percent
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
import wifi
MY_CHAT = 555
POLL_S = 10
led = Pin(board.LED, Pin.OUT)
ldr = board.analog_in(board.LDR)
bot = Bot(TELEGRAM_TOKEN)
def answer(cmd):
if cmd == "/light":
return "light %d %%" % to_percent(ldr.read_u16())
if cmd == "/on":
led.on()
return "LED on"
if cmd == "/off":
led.off()
return "LED off"
return "use /light /on /off"
wifi.connect()
while True:
try:
for chat, text in bot.updates():
if chat != MY_CHAT:
print("ignored", chat, text)
continue
reply = answer(text)
print(text, "->", reply, "| led", led.value())
bot.send(chat, reply)
except OSError as e:
print("network error", e)
time.sleep(POLL_S)/light -> light 40 % | led 0 /on -> LED on | led 1 ignored 999 /off /light -> light 70 % | led 1 hello -> use /light /on /off | led 1 network error -202 network error -202 ...
| الأمر | عمله |
|---|---|
| bot.updates() | يعيد قائمة من أزواج (معرّف المحادثة، النص)، ويحدّث offset بنفسه. |
| chat != MY_CHAT | نتجاهل كل من ليس صاحب البوت. |
| except OSError | انقطاع الشبكة حدث عادي: نبلّغ عنه ونحاول في الدورة التالية. |
chat يستطيع الغريب تشغيل ما توصله اللوحة. ولا نرسل مع الأوامر أي تفصيل لا نريد أن يقرأه من وجد البوت.chat؟18.6 كم مرة نسأل؟ – לא לבקש בכל שנייה (Polling Rate)
كل طلب HTTPS في مكتبة requests يفتح اتصالا جديدا، مع مصافحة تشفير تستهلك زمنا وذاكرة، وخلالها لا يفعل البرنامج شيئا آخر. ولذلك لا نسأل في كل ثانية.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| N | عدد الطلبات في اليوم | — |
| T | الزمن بين طلبين | s |
أسوأ حالة: وصل الأمر مباشرة بعد طلب، فينتظر الدورة كاملة، أي 10 s.
| زمن الاستطلاع | طلبات في اليوم | أطول انتظار لأمر |
|---|---|---|
| 1 s | 86400 | 1 s |
| 5 s | 17280 | 5 s |
| 10 s | 8640 | 10 s |
| 60 s | 1440 | 60 s |
- لأمر ينتظره إنسان يكفي ما بين 5 و10 ثوانٍ. ولتنبيه يأتي من اللوحة لا نحتاج إلى الاستطلاع أصلا: نرسل عند الحدث فقط.
- حسب توثيق Telegram: لا نرسل أكثر من رسالة واحدة في الثانية إلى المحادثة نفسها. ومن يتجاوز ذلك باستمرار يتلقّى خطأ بالرمز 429.
- في
getUpdatesمعامل اسمهtimeoutيجعل الخادم يُبقي الطلب مفتوحا حتى تصل رسالة. هذا يوفّر طلبات، لكن اللوحة تبقى محجوبة طوال الانتظار. سنرى في الفصل 19 لماذا يهمّنا هذا.
أهمّ أفكار الفصل:
- اللوحة هي التي تبدأ الاتصال، ولذلك يعمل البوت من خلف الموجّه دون أي إعداد.
- الرمز كلمة مرور: مكانه
secrets.py، ولا يُطبع ولا يُشارَك. وإذا تسرّب نلغيه عند BotFather أولا. - الإرسال طلب
sendMessage، والاستقبال استطلاع بالأمرgetUpdates. - من دون
offset= آخر رقم + 1 تعود الرسالة نفسها في كل مرة. - البوت يفحص معرّف المحادثة قبل أن ينفّذ أي أمر.
- الاستطلاع كل بضع ثوانٍ يكفي، وانقطاع الشبكة يُعالَج ولا يوقف البرنامج.
أوامر الفصل:
| الأمر | عمله |
|---|---|
| from secrets import TELEGRAM_TOKEN | الطريقة الوحيدة التي يصل بها الرمز إلى البرنامج. |
| POST .../bot<TOKEN>/sendMessage | إرسال رسالة؛ الحقلان chat_id وtext. |
| GET .../bot<TOKEN>/getUpdates?offset=n | التحديثات التي رقمها n فأكثر. |
| /newbot | في BotFather: إنشاء بوت. |
| /token | في BotFather: رمز جديد بدل الرمز المتسرّب. |
| Bot(token) | صنف من المكتبة tgbot.py، وله الدالتان send وupdates. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| بوت | בוט | Bot |
| رمز البوت | טוקן | Bot Token |
| معرّف المحادثة | מזהה שיחה | Chat ID |
| تحديث | עדכון | Update |
| الاستطلاع | תשאול | Polling |
| إلغاء الرمز | ביטול טוקן | Token Revocation |
status 200 فقط، لأن الشرط لا يتحقق.أسئلة للتمرين:
secrets.py، والسطر from secrets import TELEGRAM_TOKEN.offset؟secrets.py، ثم فحص أن القديم يُرفض، ثم حذف الصورة.ALLOWED = (555, 777)
for chat, text in bot.updates():
if chat not in ALLOWED:
continue
bot.send(chat, answer(text))answer الأمر /status الذي يعيد حالة الثنائي الباعث للضوء.if cmd == "/status":، ثم نعيد "LED %d" % led.value().import time
import board
from convert import to_percent
from secrets import TELEGRAM_TOKEN
from tgbot import Bot
MY_CHAT = 555
ldr = board.analog_in(board.LDR)
bot = Bot(TELEGRAM_TOKEN)
dark = False
while True:
level = to_percent(ldr.read_u16())
if level < 20 and not dark:
dark = True
bot.send(MY_CHAT, "it is dark: %d %%" % level)
elif level > 30:
dark = False
time.sleep(10)