arena commited on
Commit ·
eef656f
1
Parent(s): 13a0927
تنبيه بدء البثّ المباشر إلى تيليغرام + حذف الحسابين غير الموجودين
Browse files- telegram_api: notify_live_start()/notify_targets()/live_start_text() — رسالة باسم
المذيع والتاريخ والوقت إلى القناة والحساب الشخصي، مرة لكل بثّ (منع التكرار بـroom_id
في السجلّ الدائم)، وأي فشل يُسجَّل ولا يرمي استثناءً.
- app.py: خطّاف في حلقة المراقبة قبل أول اتصال بالمصدر + POST /api/telegram/notify-test.
- index.html: زرّ تجربة التنبيه وسطر حالة 🔔 في لوحة تيليغرام.
- Dockerfile/variables.env: LIVE_NOTIFY=on, LIVE_NOTIFY_TARGETS=both, LIVE_NOTIFY_TEXT.
- اختبارات §18 (33) ⇒ 159/159.
- Dockerfile +9 -0
- README.md +31 -0
- VARIABLES.md +5 -0
- app.py +25 -0
- index.html +9 -2
- telegram_api.py +99 -0
- test_telegram_archive.py +130 -1
- variables.env +7 -0
Dockerfile
CHANGED
|
@@ -98,6 +98,12 @@ ENV TIKTOK_TT_TARGET_IDC=useast1a
|
|
| 98 |
# مع سبب واضح في الطابور، فلا تضيع رفعة غيغابايت لتُرفض في النهاية.
|
| 99 |
# ARCHIVE_PART_MB=0 ⇒ تلقائي: أي ملف يتجاوز 95% من سقف الرفع الحالي يُقسَّم
|
| 100 |
# بـ ffmpeg -c copy (بلا إعادة ترميز) وتُرسل أجزاؤه ألبومًا واحدًا (sendMediaGroup).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
ENV ARCHIVE_ENABLED=on \
|
| 102 |
ARCHIVE_DELETE_AFTER_UPLOAD=on \
|
| 103 |
ARCHIVE_PROTECT_QUEUE=on \
|
|
@@ -112,6 +118,9 @@ ENV ARCHIVE_ENABLED=on \
|
|
| 112 |
TELEGRAM_REQUIRE_BACKUP=on \
|
| 113 |
TELEGRAM_SILENT=off \
|
| 114 |
TELEGRAM_MIN_CHAT_INTERVAL=4 \
|
|
|
|
|
|
|
|
|
|
| 115 |
TELEGRAM_LOCAL_PORT=8081 \
|
| 116 |
TELEGRAM_LOCAL_URL=http://127.0.0.1:8081 \
|
| 117 |
TZ=Asia/Baghdad
|
|
|
|
| 98 |
# مع سبب واضح في الطابور، فلا تضيع رفعة غيغابايت لتُرفض في النهاية.
|
| 99 |
# ARCHIVE_PART_MB=0 ⇒ تلقائي: أي ملف يتجاوز 95% من سقف الرفع الحالي يُقسَّم
|
| 100 |
# بـ ffmpeg -c copy (بلا إعادة ترميز) وتُرسل أجزاؤه ألبومًا واحدًا (sendMediaGroup).
|
| 101 |
+
# تنبيه بدء البثّ: حين يكتشف المراقب أنّ مذيعًا صار مباشرًا تصل رسالة إلى تيليغرام
|
| 102 |
+
# باسم المذيع وتاريخ اليوم ووقته (منطقة TZ). تُبعث **مرة واحدة لكل بثّ** لا لكل
|
| 103 |
+
# مقطع، ولا تكرار بعد إعادة الإقلاع (معرّف الغرفة محفوظ في السجلّ الدائم).
|
| 104 |
+
# LIVE_NOTIFY_TARGETS: both = القناة والحساب الشخصي | channel | backup.
|
| 105 |
+
# LIVE_NOTIFY_TEXT: قالب اختياري بالمفاتيح {name} {username} {nickname} {live_title}
|
| 106 |
+
# {date} {time} {room_id} — فارغ ⇒ النصّ الافتراضي.
|
| 107 |
ENV ARCHIVE_ENABLED=on \
|
| 108 |
ARCHIVE_DELETE_AFTER_UPLOAD=on \
|
| 109 |
ARCHIVE_PROTECT_QUEUE=on \
|
|
|
|
| 118 |
TELEGRAM_REQUIRE_BACKUP=on \
|
| 119 |
TELEGRAM_SILENT=off \
|
| 120 |
TELEGRAM_MIN_CHAT_INTERVAL=4 \
|
| 121 |
+
LIVE_NOTIFY=on \
|
| 122 |
+
LIVE_NOTIFY_TARGETS=both \
|
| 123 |
+
LIVE_NOTIFY_TEXT= \
|
| 124 |
TELEGRAM_LOCAL_PORT=8081 \
|
| 125 |
TELEGRAM_LOCAL_URL=http://127.0.0.1:8081 \
|
| 126 |
TZ=Asia/Baghdad
|
README.md
CHANGED
|
@@ -52,6 +52,34 @@ pinned: false
|
|
| 52 |
* ملاحظة تقنية مُثبتة بالتجربة: حقل الملف داخل `InputMedia` يجب أن يكون `media` لا `video`،
|
| 53 |
وإلا ردّ الخادم `can't parse InputMedia: media not found`.
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
## V15: اعتماد وجهة جديدة دون ضياع الأرشيف
|
| 56 |
|
| 57 |
1. اضغط **فحص الوجهة والبوت** لعرض الوجهة الموجودة في Secrets والوجهة المعتمدة السابقة. الفحص للقراءة فقط ولا يغيّر الربط.
|
|
@@ -220,6 +248,9 @@ ffmpeg -i in.flv -map 0:v:0 -map 0:a:0? -c copy -tag:v avc3 -movflags +faststart
|
|
| 220 |
| `TELEGRAM_SEND_AS` | `auto` | `auto` فيديو ثم ملف · `video` · `document` |
|
| 221 |
| `TELEGRAM_SILENT` | `off` | `on` = إرسال بلا إشعار للمشتركين |
|
| 222 |
| `TELEGRAM_MIN_CHAT_INTERVAL` | `4` | ثوانٍ بين إرسالين للمحادثة نفسها |
|
|
|
|
|
|
|
|
|
|
| 223 |
| `ARCHIVE_PROTECT_QUEUE` | `on` | منع التنظيف من حذف ملف لم يُرسل |
|
| 224 |
| `ARCHIVE_MIN_MB` | `5` | أصغر حجم يستحق الإرسال |
|
| 225 |
| `ARCHIVE_MAX_ATTEMPTS` | `6` | عدد المحاولات قبل التوقف مع إبقاء الملف |
|
|
|
|
| 52 |
* ملاحظة تقنية مُثبتة بالتجربة: حقل الملف داخل `InputMedia` يجب أن يكون `media` لا `video`،
|
| 53 |
وإلا ردّ الخادم `can't parse InputMedia: media not found`.
|
| 54 |
|
| 55 |
+
## تنبيه بدء البثّ المباشر
|
| 56 |
+
|
| 57 |
+
حين يكتشف المراقب أنّ أحد المذيعين صار **مباشرًا** تصل رسالة إلى تيليغرام فورًا،
|
| 58 |
+
قبل أول محاولة اتصال بمصدر الفيديو:
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
🔴 أحمد (@ahmedbarbooka2) بدأ بثًّا مباشرًا
|
| 62 |
+
📅 10-10-2026 · ⏰ 20:30
|
| 63 |
+
📝 سهرة الليلة
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
* **الوجهة:** القناة **و** الحساب الشخصي معًا (`LIVE_NOTIFY_TARGETS=both` الافتراضي؛
|
| 67 |
+
`channel` أو `backup` لوجهة واحدة).
|
| 68 |
+
* **المحتوى:** لقب المذيع كما يظهر في تيك توك (وإلا اسم المستخدم)، `@username`،
|
| 69 |
+
تاريخ اليوم ووقته بمنطقة `TZ=Asia/Baghdad`، وعنوان البثّ إن وُجد.
|
| 70 |
+
* **مرة واحدة لكل بثّ:** لا لكل مقطع (البثّ يتجزّأ عند انقطاع المصدر)، ولا بعد إعادة
|
| 71 |
+
الإقلاع (معرّف الغرفة محفوظ في `telegram-api-state.json`). حين يعود المذيع إلى
|
| 72 |
+
«البث منتهٍ» ثم يبدأ بثًّا جديدًا يُعلَن من جديد.
|
| 73 |
+
* **بلا مخاطرة:** التنبيه خدمة إضافية — أي فشل إرسال يُسجَّل في سجلّ التطبيق ولا
|
| 74 |
+
يوقف التسجيل ولا الأرشفة، ولا تُعاد المحاولة (حتى لا تتكرّر الرسالة).
|
| 75 |
+
* **بلا رسالة انتهاء:** المطلوب كان إعلان البدء فقط، فلا رسالة عند انتهاء البثّ ولا
|
| 76 |
+
عند جهوز الأرشيف (الأرشيف يصل كالمعتاد بملفه أو ألبومه).
|
| 77 |
+
* **القالب:** `LIVE_NOTIFY_TEXT` بمفاتيح `{name} {nickname} {username} {live_title}
|
| 78 |
+
{title} {date} {time} {room_id}`، و`LIVE_NOTIFY=off` يطفئ التنبيه.
|
| 79 |
+
* **التجربة:** زرّ **تجربة تنبيه بدء البثّ** في لوحة تيليغرام (أو
|
| 80 |
+
`POST /api/telegram/notify-test`) يرسل نموذجًا مُعلَّمًا كرسالة تجريبية إلى الوجهات
|
| 81 |
+
المضبوطة، ولا يلوّث مانع التكرار.
|
| 82 |
+
|
| 83 |
## V15: اعتماد وجهة جديدة دون ضياع الأرشيف
|
| 84 |
|
| 85 |
1. اضغط **فحص الوجهة والبوت** لعرض الوجهة الموجودة في Secrets والوجهة المعتمدة السابقة. الفحص للقراءة فقط ولا يغيّر الربط.
|
|
|
|
| 248 |
| `TELEGRAM_SEND_AS` | `auto` | `auto` فيديو ثم ملف · `video` · `document` |
|
| 249 |
| `TELEGRAM_SILENT` | `off` | `on` = إرسال بلا إشعار للمشتركين |
|
| 250 |
| `TELEGRAM_MIN_CHAT_INTERVAL` | `4` | ثوانٍ بين إرسالين للمحادثة نفسها |
|
| 251 |
+
| `LIVE_NOTIFY` | `on` | تنبيه «بدأ بثًّا مباشرًا» بالاسم والتاريخ والوقت |
|
| 252 |
+
| `LIVE_NOTIFY_TARGETS` | `both` | `both` = القناة والحساب الشخصي / `channel` / `backup` |
|
| 253 |
+
| `LIVE_NOTIFY_TEXT` | _(فارغ)_ | قالب رسالة التنبيه (`{name} {username} {date} {time} …`) |
|
| 254 |
| `ARCHIVE_PROTECT_QUEUE` | `on` | منع التنظيف من حذف ملف لم يُرسل |
|
| 255 |
| `ARCHIVE_MIN_MB` | `5` | أصغر حجم يستحق الإرسال |
|
| 256 |
| `ARCHIVE_MAX_ATTEMPTS` | `6` | عدد المحاولات قبل التوقف مع إبقاء الملف |
|
VARIABLES.md
CHANGED
|
@@ -51,6 +51,11 @@
|
|
| 51 |
| `TELEGRAM_SEND_AS` | `auto` | `auto`: `sendVideo` أولًا (مشغّل داخل القناة) ثم `sendDocument` عند الرفض. `video` أو `document` لإجبار أحدهما. |
|
| 52 |
| `TELEGRAM_SILENT` | `off` | `on` = إرسال بلا إشعار للمشتركين (`disable_notification`). |
|
| 53 |
| `TELEGRAM_MIN_CHAT_INTERVAL` | `4` | أقل فاصلة بالثواني بين إرسالين للمحادثة نفسها. حارس معدل يمنع 429 عند جهوزية عدة ملفات معًا. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
| `TELEGRAM_LOCAL_URL` | `http://127.0.0.1:8081` | عنوان خادم Bot API المحلي. التطبيق يفحصه كل 60 ثانية ويستعمله إن أجاب. |
|
| 55 |
| `TELEGRAM_LOCAL_PORT` | `8081` | المنفذ الذي يشغّل عليه `entrypoint.sh` الخادم المحلي. |
|
| 56 |
| `TELEGRAM_UPLOAD_TIMEOUT` | *(محسوب)* | مهلة الإرسال بالثواني. تُحسب تلقائيًا من الحجم (~4 دقائق/غيغابايت + هامش). |
|
|
|
|
| 51 |
| `TELEGRAM_SEND_AS` | `auto` | `auto`: `sendVideo` أولًا (مشغّل داخل القناة) ثم `sendDocument` عند الرفض. `video` أو `document` لإجبار أحدهما. |
|
| 52 |
| `TELEGRAM_SILENT` | `off` | `on` = إرسال بلا إشعار للمشتركين (`disable_notification`). |
|
| 53 |
| `TELEGRAM_MIN_CHAT_INTERVAL` | `4` | أقل فاصلة بالثواني بين إرسالين للمحادثة نفسها. حارس معدل يمنع 429 عند جهوزية عدة ملفات معًا. |
|
| 54 |
+
| `LIVE_NOTIFY` | `on` | تنبيه «بدأ بثًّا مباشرًا» إلى تيليغرام باسم المذيع وتاريخ اليوم ووقته. `off` يوقفه **بلا أي أثر على التسجيل أو الأرشفة**. |
|
| 55 |
+
| `LIVE_NOTIFY_TARGETS` | `both` | وجهة التنبيه: `both` = القناة **و** الحساب الشخصي (`TELEGRAM_CHAT_ID` + `TELEGRAM_BACKUP_CHAT_ID`)، أو `channel`، أو `backup`. |
|
| 56 |
+
| `LIVE_NOTIFY_TEXT` | _(فارغ)_ | قالب اختياري بمفاتيح `{name}` `{nickname}` `{username}` `{live_title}` `{title}` `{date}` `{time}` `{room_id}`. فارغ ⇒ النصّ الافتراضي: «🔴 الاسم (@المستخدم) بدأ بثًّا مباشرًا / 📅 التاريخ · ⏰ الوقت» ويُضاف سطر العنوان إن وُجد. مفتاح غير معروف يبقى كما هو ولا يُسقط الرسالة. |
|
| 57 |
+
|
| 58 |
+
**كيف لا يتكرّر التنبيه:** يُبعث **مرة واحدة لكل بثّ** لا لكل مقطع — البثّ الواحد يتجزّأ إلى عدة ملفات عند انقطاع المصدر ثم عودته، وإعادة الإعلان إزعاج. المنع على طبقتين: في ذاكرة المراقب (يُمسح حين يعود المذيع إلى «البث منتهٍ» فيُعلَن بثّه القادم)، وفي السجلّ الدائم `telegram-api-state.json` بمعرّف الغرفة `room_id` (فلا تكرار بعد إعادة إقلاع أثناء بثّ قائم). ومفتاح غير معروف في القالب لا يُسقط الرسالة، وأي فشل إرسال يُسجَّل في سجلّ التطبيق ولا يوقف التسجيل أبدًا. للتجربة من اللوحة: زرّ **تجربة تنبيه بدء البثّ** (أو `POST /api/telegram/notify-test`) يرسل نموذجًا مُعلَّمًا كرسالة تجريبية.
|
| 59 |
| `TELEGRAM_LOCAL_URL` | `http://127.0.0.1:8081` | عنوان خادم Bot API المحلي. التطبيق يفحصه كل 60 ثانية ويستعمله إن أجاب. |
|
| 60 |
| `TELEGRAM_LOCAL_PORT` | `8081` | المنفذ الذي يشغّل عليه `entrypoint.sh` الخادم المحلي. |
|
| 61 |
| `TELEGRAM_UPLOAD_TIMEOUT` | *(محسوب)* | مهلة الإرسال بالثواني. تُحسب تلقائيًا من الحجم (~4 دقائق/غيغابايت + هامش). |
|
app.py
CHANGED
|
@@ -347,6 +347,9 @@ PROTECT_QUEUE=os.getenv('ARCHIVE_PROTECT_QUEUE','on').strip().lower() in ('1','o
|
|
| 347 |
# 0 يُعطّل الضمّ ويرفع كل ملف فور جهوزه.
|
| 348 |
JOIN_WINDOW=max(0,int(os.getenv('ARCHIVE_JOIN_WINDOW','300') or 0))
|
| 349 |
_live_meta={}
|
|
|
|
|
|
|
|
|
|
| 350 |
_NICKS=ROOT/'nicknames.json'
|
| 351 |
|
| 352 |
def load_nicknames():
|
|
@@ -1426,7 +1429,18 @@ def worker(user,stop):
|
|
| 1426 |
remember_nickname(user,known.get('nickname'))
|
| 1427 |
except Exception:pass
|
| 1428 |
if not urls:
|
|
|
|
| 1429 |
update(user,status='البث منتهٍ');stop.wait(INTERVAL);continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1430 |
update(user,status='جاري الاتصال بمصدر الفيديو');ok=False
|
| 1431 |
for url in urls:
|
| 1432 |
if stop.is_set():break
|
|
@@ -1475,6 +1489,17 @@ def telegram_check():
|
|
| 1475 |
msg=str(e) if isinstance(e,(APIError,RuntimeError,ValueError)) else 'تعذر فحص وجهة تيليغرام'
|
| 1476 |
raise HTTPException(400,msg)
|
| 1477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1478 |
class TelegramChannelConfirmation(BaseModel):
|
| 1479 |
channel_id:str
|
| 1480 |
@app.post('/api/telegram/channel')
|
|
|
|
| 347 |
# 0 يُعطّل الضمّ ويرفع كل ملف فور جهوزه.
|
| 348 |
JOIN_WINDOW=max(0,int(os.getenv('ARCHIVE_JOIN_WINDOW','300') or 0))
|
| 349 |
_live_meta={}
|
| 350 |
+
# تنبيه «بدأ بثًّا مباشرًا»: مرة واحدة لكل بثّ. المفتاح اسم المذيع والقيمة معرّف
|
| 351 |
+
# الغرفة (أو True إن لم يُعرف بعد)؛ يُمسح حين ينتهي البثّ فيُعلَن البثّ القادم.
|
| 352 |
+
_live_seen={}
|
| 353 |
_NICKS=ROOT/'nicknames.json'
|
| 354 |
|
| 355 |
def load_nicknames():
|
|
|
|
| 1429 |
remember_nickname(user,known.get('nickname'))
|
| 1430 |
except Exception:pass
|
| 1431 |
if not urls:
|
| 1432 |
+
_live_seen.pop(user,None)
|
| 1433 |
update(user,status='البث منتهٍ');stop.wait(INTERVAL);continue
|
| 1434 |
+
# تنبيه بدء البثّ قبل أول محاولة اتصال: مرة لكل بثّ لا لكل مقطع (البثّ
|
| 1435 |
+
# الواحد يتجزّأ عند انقطاع المصدر)، ومنع التكرار قائم أيضًا في
|
| 1436 |
+
# telegram_api بمعرّف الغرفة حتى لو أُعيد إقلاع البرنامج أثناء البثّ.
|
| 1437 |
+
# أي فشل هنا لا يوقف التسجيل: التنبيه خدمة إضافية لا شرط.
|
| 1438 |
+
if not _live_seen.get(user):
|
| 1439 |
+
meta=_live_meta.get(user) or {}
|
| 1440 |
+
room=str(meta.get('room_id') or '')
|
| 1441 |
+
_live_seen[user]=room or True
|
| 1442 |
+
try:telegram.notify_live_start(user,meta,room)
|
| 1443 |
+
except Exception:log(user,'تعذّر إرسال تنبيه بدء البثّ')
|
| 1444 |
update(user,status='جاري الاتصال بمصدر الفيديو');ok=False
|
| 1445 |
for url in urls:
|
| 1446 |
if stop.is_set():break
|
|
|
|
| 1489 |
msg=str(e) if isinstance(e,(APIError,RuntimeError,ValueError)) else 'تعذر فحص وجهة تيليغرام'
|
| 1490 |
raise HTTPException(400,msg)
|
| 1491 |
|
| 1492 |
+
@app.post('/api/telegram/notify-test')
|
| 1493 |
+
def telegram_notify_test():
|
| 1494 |
+
"""زرّ تجربة: يرسل نموذج تنبيه بدء البثّ إلى الوجهات المضبوطة (بلا بثّ فعلي)."""
|
| 1495 |
+
try:
|
| 1496 |
+
ok=telegram.notify_live_start('example',{'nickname':'مثال','live_title':'تجربة تنبيه بدء البثّ'},test=True)
|
| 1497 |
+
return {'ok':bool(ok),'targets':telegram.notify_targets()}
|
| 1498 |
+
except Exception as e:
|
| 1499 |
+
from telegram_api import APIError
|
| 1500 |
+
msg=str(e) if isinstance(e,(APIError,RuntimeError,ValueError)) else 'تعذر إرسال تنبيه تجريبي'
|
| 1501 |
+
raise HTTPException(400,msg)
|
| 1502 |
+
|
| 1503 |
class TelegramChannelConfirmation(BaseModel):
|
| 1504 |
channel_id:str
|
| 1505 |
@app.post('/api/telegram/channel')
|
index.html
CHANGED
|
@@ -30,7 +30,7 @@ textarea,select{font:inherit;background:#0c1220;color:#e4eaf7;border:1px solid #
|
|
| 30 |
</section>
|
| 31 |
<section class="panel"><h2>ربط قناة تيليغرام</h2>
|
| 32 |
<p>يوتيوب أُلغي من هذا المشروع نهائيًا. الوجهة الآن <b>تيليغرام</b>: تُقرأ من السرّين <code dir="ltr">TELEGRAM_CHAT_ID</code> (القناة) و<code dir="ltr">TELEGRAM_BACKUP_CHAT_ID</code> (حسابك الشخصي). الفحص يقرأ الوجهة من تيليغرام نفسه ولا يغيّر الربط.</p>
|
| 33 |
-
<div class="row"><button id="tgcheck" class="secondary" type="button">فحص الوجهة والبوت</button><button id="tgadopt" type="button" hidden>اعتماد الوجهة الجديدة فقط</button></div>
|
| 34 |
<p id="tgchannelnotice" class="notice" role="status" aria-live="polite" hidden></p>
|
| 35 |
<p id="channel">لم يتم فحص الوجهة بعد.</p><p id="tgstate">تحميل الحالة…</p>
|
| 36 |
<small id="budget"></small>
|
|
@@ -61,7 +61,7 @@ function error(text){$('error').textContent=text;$('error').hidden=!text}
|
|
| 61 |
function channelSwitchPending(){return !!(checkedChannel?.previous&&checkedChannel.previous.id!==checkedChannel.id)}
|
| 62 |
function renderCheckedChannel(){
|
| 63 |
const pending=channelSwitchPending(),needsAdoption=!!(checkedChannel&&String(checkedChannel.id)!==String(lastT?.channel?.id));
|
| 64 |
-
$('tgadopt').hidden=!needsAdoption;$('tgadopt').disabled=tgBusy;$('tgcheck').disabled=tgBusy;
|
| 65 |
$('tgchannelnotice').hidden=!checkedChannel;
|
| 66 |
$('tgchannelnotice').textContent=!checkedChannel?'':pending
|
| 67 |
? 'الوجهة المتاحة في الأسرار: «'+checkedChannel.title+'» ('+checkedChannel.id+') بدل «'+checkedChannel.previous.title+'». الفحص لم يبدّل الربط. اعتمد الجديدة بتأكيدك؛ يُحفظ طابور الوجهة السابقة وتُوقف الأرشفة حتى تفعيلها من جديد.'
|
|
@@ -79,6 +79,7 @@ $('capacity').textContent=`${s.version} • حد الحسابات: ${s.max_accou
|
|
| 79 |
$('channel').textContent=y.channel?`الوجهة المعتمدة: ${y.channel.title} · ${y.channel.id}${y.channel.username?' · @'+y.channel.username:''}`:'اضبط TELEGRAM_CHAT_ID ثم اضغط فحص الوجهة.';
|
| 80 |
renderCheckedChannel();
|
| 81 |
$('tgstate').textContent=channelSwitchPending()?'الوجهة في الأسرار مختلفة؛ اعتمد الجديدة قبل تفعيل الأرشفة.':y.configured?`البوت ${y.bot_username||'مضبوط'} — سقف الرفع ${y.max_upload_mb}MB عبر ${y.local_server?'خادم Bot API المحلي':'الواجهة العامة (50MB فقط؛ أضف TELEGRAM_API_ID/HASH لرفعه)'}. النسخة الاحتياطية: ${y.backup_configured?'مضبوطة ✓':'غير مضبوطة'}.${y.local_server||!y.local_error||y.local_error.indexOf('TELEGRAM_API_ID')===0?'':'\n⚠ الخادم المحلي لا يستجيب: '+y.local_error}`:'TELEGRAM_BOT_TOKEN أو TELEGRAM_CHAT_ID ناقص — لن يُرسل أي تسجيل.';
|
|
|
|
| 82 |
$('budget').textContent=`أُرسل اليوم: ${y.budget?.sent||0} ملف · ${Math.round((y.budget?.bytes||0)/1048576)}MB`;
|
| 83 |
$('tasks').innerHTML=s.tasks.length?s.tasks.map(t=>`<article class="card"><h3>@${esc(t.username)}</h3><p>${esc(t.status)}</p><small>${esc(t.quality||'الجودة لم تحدد بعد')} · ${t.received_mb||0} MB · ${Math.floor((t.elapsed_seconds||0)/60)} دقيقة</small><p>${t.enabled?'المراقبة مفعّلة':'المراقبة معطّلة'} · توكن الجلسة: ${t.has_token?'موجود ✓':'غير مضاف'}</p><div class="row"><button data-user="${esc(t.username)}" data-action="start">تشغيل</button><button class="secondary" data-user="${esc(t.username)}" data-action="stop">إيقاف</button><button class="secondary" data-user="${esc(t.username)}" data-action="token">توكن</button><button class="danger" data-user="${esc(t.username)}" data-action="delete">حذف</button></div></article>`).join(''):'<p>أضف حسابات المذيع المطلوبة فقط.</p>';
|
| 84 |
$('files').innerHTML=s.files.length?s.files.map(f=>`<div class="file"><div><code>${esc(f.name)}</code><br><small>${f.mb} MB</small></div><div class="row"><a href="/api/files/${encodeURIComponent(f.name)}">تنزيل</a><button class="danger" data-file="${esc(f.name)}">حذف الملف</button></div></div>`).join(''):'<p>لا توجد ملفات مكتملة بعد.</p>';
|
|
@@ -127,6 +128,12 @@ $('tgcheck').onclick=async()=>{
|
|
| 127 |
alert('الوجهة المتاحة في الأسرار: «'+channel.title+'» ('+channel.id+').'+(channelSwitchPending()?' تحتاج تأكيد اعتمادها؛ الفحص لم يغيّر الربط.':' الفحص للقراءة فقط.'));
|
| 128 |
}catch(e){error(e.message)}finally{tgBusy=false;renderCheckedChannel()}
|
| 129 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
$('tgadopt').onclick=async()=>{
|
| 131 |
if(tgBusy)return;tgBusy=true;renderCheckedChannel();
|
| 132 |
try{
|
|
|
|
| 30 |
</section>
|
| 31 |
<section class="panel"><h2>ربط قناة تيليغرام</h2>
|
| 32 |
<p>يوتيوب أُلغي من هذا المشروع نهائيًا. الوجهة الآن <b>تيليغرام</b>: تُقرأ من السرّين <code dir="ltr">TELEGRAM_CHAT_ID</code> (القناة) و<code dir="ltr">TELEGRAM_BACKUP_CHAT_ID</code> (حسابك الشخصي). الفحص يقرأ الوجهة من تيليغرام نفسه ولا يغيّر الربط.</p>
|
| 33 |
+
<div class="row"><button id="tgcheck" class="secondary" type="button">فحص الوجهة والبوت</button><button id="tgnotify" class="secondary" type="button">تجربة تنبيه بدء البثّ</button><button id="tgadopt" type="button" hidden>اعتماد الوجهة الجديدة فقط</button></div>
|
| 34 |
<p id="tgchannelnotice" class="notice" role="status" aria-live="polite" hidden></p>
|
| 35 |
<p id="channel">لم يتم فحص الوجهة بعد.</p><p id="tgstate">تحميل الحالة…</p>
|
| 36 |
<small id="budget"></small>
|
|
|
|
| 61 |
function channelSwitchPending(){return !!(checkedChannel?.previous&&checkedChannel.previous.id!==checkedChannel.id)}
|
| 62 |
function renderCheckedChannel(){
|
| 63 |
const pending=channelSwitchPending(),needsAdoption=!!(checkedChannel&&String(checkedChannel.id)!==String(lastT?.channel?.id));
|
| 64 |
+
$('tgadopt').hidden=!needsAdoption;$('tgadopt').disabled=tgBusy;$('tgcheck').disabled=tgBusy;$('tgnotify').disabled=tgBusy;
|
| 65 |
$('tgchannelnotice').hidden=!checkedChannel;
|
| 66 |
$('tgchannelnotice').textContent=!checkedChannel?'':pending
|
| 67 |
? 'الوجهة المتاحة في الأسرار: «'+checkedChannel.title+'» ('+checkedChannel.id+') بدل «'+checkedChannel.previous.title+'». الفحص لم يبدّل الربط. اعتمد الجديدة بتأكيدك؛ يُحفظ طابور الوجهة السابقة وتُوقف الأرشفة حتى تفعيلها من جديد.'
|
|
|
|
| 79 |
$('channel').textContent=y.channel?`الوجهة المعتمدة: ${y.channel.title} · ${y.channel.id}${y.channel.username?' · @'+y.channel.username:''}`:'اضبط TELEGRAM_CHAT_ID ثم اضغط فحص الوجهة.';
|
| 80 |
renderCheckedChannel();
|
| 81 |
$('tgstate').textContent=channelSwitchPending()?'الوجهة في الأسرار مختلفة؛ اعتمد الجديدة قبل تفعيل الأرشفة.':y.configured?`البوت ${y.bot_username||'مضبوط'} — سقف الرفع ${y.max_upload_mb}MB عبر ${y.local_server?'خادم Bot API المحلي':'الواجهة العامة (50MB فقط؛ أضف TELEGRAM_API_ID/HASH لرفعه)'}. النسخة الاحتياطية: ${y.backup_configured?'مضبوطة ✓':'غير مضبوطة'}.${y.local_server||!y.local_error||y.local_error.indexOf('TELEGRAM_API_ID')===0?'':'\n⚠ الخادم المحلي لا يستجيب: '+y.local_error}`:'TELEGRAM_BOT_TOKEN أو TELEGRAM_CHAT_ID ناقص — لن يُرسل أي تسجيل.';
|
| 82 |
+
if(y.configured)$('tgstate').textContent+='\n'+(y.live_notify?'🔔 تنبيه بدء البثّ مفعّل → '+((y.live_notify_targets||[]).length>1?'القناة والحساب الشخصي':((y.live_notify_targets||[]).length===1?'وجهة واحدة':'لا وجهة مضبوطة')):'🔕 تنبيه بدء البثّ مُطفأ (LIVE_NOTIFY=off)');
|
| 83 |
$('budget').textContent=`أُرسل اليوم: ${y.budget?.sent||0} ملف · ${Math.round((y.budget?.bytes||0)/1048576)}MB`;
|
| 84 |
$('tasks').innerHTML=s.tasks.length?s.tasks.map(t=>`<article class="card"><h3>@${esc(t.username)}</h3><p>${esc(t.status)}</p><small>${esc(t.quality||'الجودة لم تحدد بعد')} · ${t.received_mb||0} MB · ${Math.floor((t.elapsed_seconds||0)/60)} دقيقة</small><p>${t.enabled?'المراقبة مفعّلة':'المراقبة معطّلة'} · توكن الجلسة: ${t.has_token?'موجود ✓':'غير مضاف'}</p><div class="row"><button data-user="${esc(t.username)}" data-action="start">تشغيل</button><button class="secondary" data-user="${esc(t.username)}" data-action="stop">إيقاف</button><button class="secondary" data-user="${esc(t.username)}" data-action="token">توكن</button><button class="danger" data-user="${esc(t.username)}" data-action="delete">حذف</button></div></article>`).join(''):'<p>أضف حسابات المذيع المطلوبة فقط.</p>';
|
| 85 |
$('files').innerHTML=s.files.length?s.files.map(f=>`<div class="file"><div><code>${esc(f.name)}</code><br><small>${f.mb} MB</small></div><div class="row"><a href="/api/files/${encodeURIComponent(f.name)}">تنزيل</a><button class="danger" data-file="${esc(f.name)}">حذف الملف</button></div></div>`).join(''):'<p>لا توجد ملفات مكتملة بعد.</p>';
|
|
|
|
| 128 |
alert('الوجهة المتاحة في الأسرار: «'+channel.title+'» ('+channel.id+').'+(channelSwitchPending()?' تحتاج تأكيد اعتمادها؛ الفحص لم يغيّر الربط.':' الفحص للقراءة فقط.'));
|
| 129 |
}catch(e){error(e.message)}finally{tgBusy=false;renderCheckedChannel()}
|
| 130 |
};
|
| 131 |
+
$('tgnotify').onclick=async()=>{
|
| 132 |
+
if(tgBusy)return;tgBusy=true;renderCheckedChannel();
|
| 133 |
+
try{error('');const r=await api('/api/telegram/notify-test','POST');
|
| 134 |
+
alert(r.ok?('أُرسل نموذج التنبيه إلى '+((r.targets||[]).length)+' وجهة — تفقّد القناة ورسائل البوت الخاصة.'):'لم يُرسل التنبيه: تحقّق من LIVE_NOTIFY ومن ضبط القناة والنسخة الاحتياطية.');
|
| 135 |
+
}catch(e){error(e.message)}finally{tgBusy=false;renderCheckedChannel()}
|
| 136 |
+
};
|
| 137 |
$('tgadopt').onclick=async()=>{
|
| 138 |
if(tgBusy)return;tgBusy=true;renderCheckedChannel();
|
| 139 |
try{
|
telegram_api.py
CHANGED
|
@@ -36,6 +36,17 @@ ABSOLUTE_MAX_MB = 2000
|
|
| 36 |
|
| 37 |
# حاشية الوسائط في تيليغرام: 1024 حرفًا (لا 4900 كوصف يوتيوب).
|
| 38 |
CAPTION_LIMIT = 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# حدّ تيليغرام لعناصر الألبوم الواحد في sendMediaGroup.
|
| 40 |
MAX_ALBUM_ITEMS = 100
|
| 41 |
|
|
@@ -564,10 +575,98 @@ class TelegramCore:
|
|
| 564 |
'local_error': self.api.local_error(),
|
| 565 |
'max_upload_mb': self.api.max_upload_mb(),
|
| 566 |
'budget': budget,
|
|
|
|
|
|
|
|
|
|
| 567 |
'live_relay': 'removed',
|
| 568 |
'youtube': 'removed',
|
| 569 |
}
|
| 570 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
# ---------- دورة حياة ----------
|
| 572 |
def restore(self):
|
| 573 |
"""إقلاع: ينبّه على بقايا يوتيوب وعلى نقص الأسرار، ولا يرسل شيئًا."""
|
|
|
|
| 36 |
|
| 37 |
# حاشية الوسائط في تيليغرام: 1024 حرفًا (لا 4900 كوصف يوتيوب).
|
| 38 |
CAPTION_LIMIT = 1024
|
| 39 |
+
# حدّ تيليغرام لنصّ الرسالة (الحاشية 1024، النصّ 4096).
|
| 40 |
+
MESSAGE_LIMIT = 4096
|
| 41 |
+
DEFAULT_LIVE_TEXT = ('\U0001f534 {name} (@{username}) بدأ بثًّا مباشرًا\n'
|
| 42 |
+
'\U0001f4c5 {date} · \u23f0 {time}')
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class _Safe(dict):
|
| 46 |
+
'''قالب لا يسقط بمفتاح ناقص: يترك {المفتاح} كما هو.'''
|
| 47 |
+
|
| 48 |
+
def __missing__(self, key):
|
| 49 |
+
return '{' + key + '}'
|
| 50 |
# حدّ تيليغرام لعناصر الألبوم الواحد في sendMediaGroup.
|
| 51 |
MAX_ALBUM_ITEMS = 100
|
| 52 |
|
|
|
|
| 575 |
'local_error': self.api.local_error(),
|
| 576 |
'max_upload_mb': self.api.max_upload_mb(),
|
| 577 |
'budget': budget,
|
| 578 |
+
'live_notify': str(os.getenv('LIVE_NOTIFY', 'on')).strip().lower()
|
| 579 |
+
in ('1', 'on', 'true', 'yes'),
|
| 580 |
+
'live_notify_targets': self.notify_targets(),
|
| 581 |
'live_relay': 'removed',
|
| 582 |
'youtube': 'removed',
|
| 583 |
}
|
| 584 |
|
| 585 |
+
# ---------- تنبيه بدء البثّ ----------
|
| 586 |
+
def notify_targets(self, which=None):
|
| 587 |
+
"""إلى أين يذهب تنبيه بدء البثّ: `both` (افتراضي) أو `channel` أو `backup`."""
|
| 588 |
+
mode = str(which or os.getenv('LIVE_NOTIFY_TARGETS', 'both') or 'both').strip().lower()
|
| 589 |
+
out = []
|
| 590 |
+
if mode in ('both', 'channel') and self.api.chat_id():
|
| 591 |
+
out.append(self.api.chat_id())
|
| 592 |
+
if mode in ('both', 'backup') and self.api.backup_chat_id():
|
| 593 |
+
out.append(self.api.backup_chat_id())
|
| 594 |
+
return out
|
| 595 |
+
|
| 596 |
+
def live_start_text(self, user, meta=None, when=None):
|
| 597 |
+
"""نصّ التنبيه: اسم المذيع وتاريخ اليوم ووقته (منطقة `TZ`)."""
|
| 598 |
+
from datetime import datetime
|
| 599 |
+
tz = None
|
| 600 |
+
try:
|
| 601 |
+
from zoneinfo import ZoneInfo
|
| 602 |
+
tz = ZoneInfo(os.getenv('TZ', 'Asia/Baghdad') or 'UTC')
|
| 603 |
+
except Exception:
|
| 604 |
+
tz = None
|
| 605 |
+
now = when or (datetime.now(tz) if tz else datetime.now())
|
| 606 |
+
meta = dict(meta or {})
|
| 607 |
+
nickname = str(meta.get('nickname') or '').strip()
|
| 608 |
+
title = str(meta.get('live_title') or '').strip()
|
| 609 |
+
template = str(os.getenv('LIVE_NOTIFY_TEXT', '') or DEFAULT_LIVE_TEXT)
|
| 610 |
+
values = _Safe({'name': nickname or user, 'nickname': nickname or user,
|
| 611 |
+
'username': user, 'title': title or (nickname or user),
|
| 612 |
+
'live_title': title, 'date': now.strftime('%d-%m-%Y'),
|
| 613 |
+
'time': now.strftime('%H:%M'),
|
| 614 |
+
'room_id': str(meta.get('room_id') or '')})
|
| 615 |
+
try:
|
| 616 |
+
text = template.format_map(values)
|
| 617 |
+
except Exception:
|
| 618 |
+
text = DEFAULT_LIVE_TEXT.format_map(values)
|
| 619 |
+
if title and '{live_title}' not in template:
|
| 620 |
+
text = f'{text}\n\U0001f4dd {title}'
|
| 621 |
+
return text.strip()[:MESSAGE_LIMIT]
|
| 622 |
+
|
| 623 |
+
def notify_live_start(self, user, meta=None, room_id='', when=None, test=False):
|
| 624 |
+
"""رسالة «بدأ بثًّا مباشرًا». ← هل وصلت إلى وجهة واحدة على الأقل.
|
| 625 |
+
|
| 626 |
+
* مرة واحدة لكل بثّ: `app.py` يمنع التكرار بين مقاطع البثّ الواحد، والسجلّ
|
| 627 |
+
الدائم يمنعه بعد إعادة التشغيل (بمعرّف الغرفة).
|
| 628 |
+
* **لا يعطّل التسجيل أبدًا**: أي فشل يُسجَّل ويعيد `False`.
|
| 629 |
+
"""
|
| 630 |
+
if str(os.getenv('LIVE_NOTIFY', 'on')).strip().lower() not in ('1', 'on', 'true', 'yes'):
|
| 631 |
+
return False
|
| 632 |
+
if not self.api.token():
|
| 633 |
+
return False
|
| 634 |
+
targets = self.notify_targets()
|
| 635 |
+
if not targets:
|
| 636 |
+
return False
|
| 637 |
+
meta = dict(meta or {})
|
| 638 |
+
key = str(user or '').lower()
|
| 639 |
+
room = str(room_id or meta.get('room_id') or '').strip()
|
| 640 |
+
if room and not test:
|
| 641 |
+
seen = self.journal.read().get('notified') or {}
|
| 642 |
+
if str(seen.get(key) or '') == room:
|
| 643 |
+
return False # هذا البثّ أُعلن من قبل، حتى لو أُعيد الإقلاع
|
| 644 |
+
text = self.live_start_text(user, meta, when)
|
| 645 |
+
if test:
|
| 646 |
+
text = ('\U0001f9ea ' + text +
|
| 647 |
+
'\n\n(رسالة تجريبية من اللوحة — لا يوجد بثّ فعلي)')[:MESSAGE_LIMIT]
|
| 648 |
+
sent = []
|
| 649 |
+
for chat in targets:
|
| 650 |
+
try:
|
| 651 |
+
self.api.call('sendMessage', {'chat_id': str(chat), 'text': text,
|
| 652 |
+
'disable_web_page_preview': True})
|
| 653 |
+
sent.append(str(chat))
|
| 654 |
+
except Exception as exc:
|
| 655 |
+
self.log('system', f'تعذّر تنبيه بدء بثّ {user} إلى {chat} '
|
| 656 |
+
f'({type(exc).__name__})')
|
| 657 |
+
if not sent:
|
| 658 |
+
return False
|
| 659 |
+
if room and not test:
|
| 660 |
+
def mutate(d):
|
| 661 |
+
seen = d.setdefault('notified', {})
|
| 662 |
+
seen[key] = room
|
| 663 |
+
if len(seen) > 200: # لا يكبر السجلّ بلا حدّ
|
| 664 |
+
d['notified'] = dict(list(seen.items())[-200:])
|
| 665 |
+
self.journal.change(mutate)
|
| 666 |
+
self.log(user, f'أُرسل تنبيه «بدأ بثًّا مباشرًا» إلى {len(sent)} وجهة'
|
| 667 |
+
+ (' (تجربة)' if test else ''))
|
| 668 |
+
return True
|
| 669 |
+
|
| 670 |
# ---------- دورة حياة ----------
|
| 671 |
def restore(self):
|
| 672 |
"""إقلاع: ينبّه على بقايا يوتيوب وعلى نقص الأسرار، ولا يرسل شيئًا."""
|
test_telegram_archive.py
CHANGED
|
@@ -40,6 +40,7 @@ class FakeBot:
|
|
| 40 |
self._mid = 100
|
| 41 |
self.albums = [] # (chat_id, file_ids, caption)
|
| 42 |
self.fail_album = None # APIError تُرمى عند أول ألبوم
|
|
|
|
| 43 |
|
| 44 |
# --- أسرار/إعداد ---
|
| 45 |
def token(self): return os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
|
@@ -91,8 +92,13 @@ class FakeBot:
|
|
| 91 |
'width': 1080, 'height': 1920, 'duration': 60,
|
| 92 |
'file_size': Path(path).stat().st_size}}
|
| 93 |
|
| 94 |
-
def call(self, method, data=None, timeout=60):
|
| 95 |
self.calls.append((method, dict(data or {})))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
if method == 'sendVideo' or method == 'sendDocument':
|
| 97 |
if self.fail_backup:
|
| 98 |
raise self.fail_backup # يبقى قائمًا: يفشل video وdocument معًا
|
|
@@ -693,6 +699,129 @@ try:
|
|
| 693 |
finally:
|
| 694 |
TA.shutil.which, TA.subprocess.run = real_w2, real_r2
|
| 695 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
# --------------------------------------------------------------------------- #
|
| 697 |
failed = [r for r in RESULTS if not r[1]]
|
| 698 |
print('\n' + '=' * 62)
|
|
|
|
| 40 |
self._mid = 100
|
| 41 |
self.albums = [] # (chat_id, file_ids, caption)
|
| 42 |
self.fail_album = None # APIError تُرمى عند أول ألبوم
|
| 43 |
+
self.fail_send = None # APIError تُرمى عند أي sendMessage
|
| 44 |
|
| 45 |
# --- أسرار/إعداد ---
|
| 46 |
def token(self): return os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
|
|
|
| 92 |
'width': 1080, 'height': 1920, 'duration': 60,
|
| 93 |
'file_size': Path(path).stat().st_size}}
|
| 94 |
|
| 95 |
+
def call(self, method, data=None, timeout=60, retries=0):
|
| 96 |
self.calls.append((method, dict(data or {})))
|
| 97 |
+
if method == 'sendMessage':
|
| 98 |
+
if self.fail_send:
|
| 99 |
+
raise self.fail_send
|
| 100 |
+
self._mid += 1
|
| 101 |
+
return {'message_id': self._mid}
|
| 102 |
if method == 'sendVideo' or method == 'sendDocument':
|
| 103 |
if self.fail_backup:
|
| 104 |
raise self.fail_backup # يبقى قائمًا: يفشل video وdocument معًا
|
|
|
|
| 699 |
finally:
|
| 700 |
TA.shutil.which, TA.subprocess.run = real_w2, real_r2
|
| 701 |
|
| 702 |
+
print('\n=== 18) تنبيه «بدأ بثًّا مباشرًا» إلى تيليغرام ===')
|
| 703 |
+
# المطلوب: اسم المذيع + التاريخ + الوقت، إلى القناة والحساب الشخصي معًا، مرة واحدة
|
| 704 |
+
# لكل بثّ (لا لكل مقطع، ولا بعد إعادة الإقلاع)، وبلا تعطيل التسجيل عند الفشل.
|
| 705 |
+
from datetime import datetime
|
| 706 |
+
|
| 707 |
+
with tempfile.TemporaryDirectory() as tmp:
|
| 708 |
+
arc, core, files = make_archive(tmp, LIVE_NOTIFY='on', LIVE_NOTIFY_TARGETS='both',
|
| 709 |
+
LIVE_NOTIFY_TEXT='')
|
| 710 |
+
bot = core.api
|
| 711 |
+
texts = lambda api: [d['text'] for m, d in api.calls if m == 'sendMessage']
|
| 712 |
+
chats = lambda api: [str(d['chat_id']) for m, d in api.calls if m == 'sendMessage']
|
| 713 |
+
|
| 714 |
+
# 1) بدء البثّ: رسالة إلى الوجهتين تحمل الاسم والتاريخ والوقت
|
| 715 |
+
ok = core.notify_live_start('ahmedbarbooka2', {'nickname': 'أحمد', 'live_title': 'سهرة',
|
| 716 |
+
'room_id': '74'},
|
| 717 |
+
when=datetime(2026, 10, 10, 8, 5))
|
| 718 |
+
check('التنبيه نجح', ok is True)
|
| 719 |
+
check('إلى القناة والحساب الشخصي معًا',
|
| 720 |
+
chats(bot) == ['-1001234567890', '8482164917'], str(chats(bot)))
|
| 721 |
+
t1 = texts(bot)[0]
|
| 722 |
+
check('يحمل اسم المذيع', 'أحمد' in t1, t1)
|
| 723 |
+
check('يحمل اسم المستخدم', '@ahmedbarbooka2' in t1, t1)
|
| 724 |
+
check('يحمل التاريخ', '10-10-2026' in t1, t1)
|
| 725 |
+
check('يحمل الوقت', '08:05' in t1, t1)
|
| 726 |
+
check('يحمل عنوان البثّ', 'سهرة' in t1, t1)
|
| 727 |
+
check('يقول إنّه بدأ بثًّا مباشرًا', 'بثًّا مباشرًا' in t1, t1)
|
| 728 |
+
check('بلا معاينة رابط (لا بطاقة مزعجة)',
|
| 729 |
+
all(d.get('disable_web_page_preview') is True for m, d in bot.calls
|
| 730 |
+
if m == 'sendMessage'))
|
| 731 |
+
|
| 732 |
+
# 2) مقطع ثانٍ من البثّ نفسه (انقطاع مصدر ثم عودة): لا تكرار
|
| 733 |
+
n0 = len(bot.calls)
|
| 734 |
+
check('المقطع الثاني من البثّ نفسه لا يُعلن مرتين',
|
| 735 |
+
core.notify_live_start('ahmedbarbooka2', {'nickname': 'أحمد', 'room_id': '74'}) is False
|
| 736 |
+
and len(bot.calls) == n0, str(len(bot.calls) - n0))
|
| 737 |
+
|
| 738 |
+
# 3) إعادة إقلاع أثناء البثّ: السجلّ الدائم يمنع التكرار
|
| 739 |
+
jpath = Path(tmp) / 'telegram-api-state.json'
|
| 740 |
+
core2 = TelegramCore(lambda *a, **k: None, jpath)
|
| 741 |
+
core2.api = FakeBot(core2.journal)
|
| 742 |
+
check('مانع التكرار محفوظ في السجلّ',
|
| 743 |
+
(core2.journal.read().get('notified') or {}).get('ahmedbarbooka2') == '74',
|
| 744 |
+
str(core2.journal.read().get('notified')))
|
| 745 |
+
check('بعد إعادة الإقلاع لا يُعلن البثّ نفسه مرّة أخرى',
|
| 746 |
+
core2.notify_live_start('ahmedbarbooka2', {'nickname': 'أحمد', 'room_id': '74'}) is False
|
| 747 |
+
and not chats(core2.api))
|
| 748 |
+
|
| 749 |
+
# 4) بثّ جديد (غرفة جديدة) ⇒ تنبيه جديد بتاريخه ووقته
|
| 750 |
+
check('بثّ جديد بغرفة جديدة يُعلن',
|
| 751 |
+
core2.notify_live_start('ahmedbarbooka2', {'nickname': 'أحمد', 'room_id': '99'},
|
| 752 |
+
when=datetime(2026, 10, 11, 20, 30)) is True)
|
| 753 |
+
t2 = texts(core2.api)[-1]
|
| 754 |
+
check('التنبيه الجديد يحمل تاريخه ووقته', '11-10-2026' in t2 and '20:30' in t2, t2)
|
| 755 |
+
|
| 756 |
+
# 5) بلا لقب ⇒ يُذكر اسم المستخدم؛ بلا عنوان ⇒ لا سطر عنوان
|
| 757 |
+
core2.notify_live_start('strongarmor', {}, when=datetime(2026, 10, 10, 9, 0))
|
| 758 |
+
t3 = texts(core2.api)[-1]
|
| 759 |
+
check('بلا لقب يُذكر اسم المستخدم', '@strongarmor' in t3 and 'strongarmor (' in t3, t3)
|
| 760 |
+
check('بلا عنوان لا يُضاف سطر العنوان', '\U0001f4dd' not in t3, t3)
|
| 761 |
+
check('لا يبقى قوس قالب غير مُعبَّأ', '{' not in t3, t3)
|
| 762 |
+
|
| 763 |
+
# 6) الإطفاء
|
| 764 |
+
os.environ['LIVE_NOTIFY'] = 'off'
|
| 765 |
+
n1 = len(core2.api.calls)
|
| 766 |
+
check('LIVE_NOTIFY=off يوقف التنبيه',
|
| 767 |
+
core2.notify_live_start('ahmedbarbooka2', {'room_id': '500'}) is False
|
| 768 |
+
and len(core2.api.calls) == n1)
|
| 769 |
+
os.environ['LIVE_NOTIFY'] = 'on'
|
| 770 |
+
|
| 771 |
+
# 7) اختيار الوجهة
|
| 772 |
+
os.environ['LIVE_NOTIFY_TARGETS'] = 'channel'
|
| 773 |
+
check('targets=channel ⇒ القناة فقط', core2.notify_targets() == ['-1001234567890'],
|
| 774 |
+
str(core2.notify_targets()))
|
| 775 |
+
os.environ['LIVE_NOTIFY_TARGETS'] = 'backup'
|
| 776 |
+
check('targets=backup ⇒ الحساب الشخصي فقط', core2.notify_targets() == ['8482164917'],
|
| 777 |
+
str(core2.notify_targets()))
|
| 778 |
+
os.environ['LIVE_NOTIFY_TARGETS'] = 'both'
|
| 779 |
+
check('targets=both ⇒ الوجهتان', len(core2.notify_targets()) == 2)
|
| 780 |
+
|
| 781 |
+
# 8) فشل الإرسال: لا استثناء، لا تعطيل للتسجيل، ويُبلَّغ
|
| 782 |
+
# make_archive يعطي النواة مسجِّلًا صامتًا؛ هنا نلتقط سجلّها كما تفعل app.py.
|
| 783 |
+
CORE_LOG = []
|
| 784 |
+
core.log = lambda user, msg: CORE_LOG.append((user, msg))
|
| 785 |
+
bot.fail_send = APIError('networkOrInvalidResponse', 'انقطاع الاتصال')
|
| 786 |
+
check('فشل الإرسال يعيد False بلا استثناء',
|
| 787 |
+
core.notify_live_start('ahmadbarbooka.cinderella', {'nickname': 'سندريلا',
|
| 788 |
+
'room_id': '1'}) is False)
|
| 789 |
+
check('الفشل سُجّل في سجلّ التطبيق',
|
| 790 |
+
any('تنبيه بدء بثّ' in m for _, m in CORE_LOG), str(CORE_LOG[:2]))
|
| 791 |
+
check('الفشل لم يُسجَّل في مانع التكرار (يُحاول في بثّ لاحق)',
|
| 792 |
+
(core.journal.read().get('notified') or {}).get('ahmadbarbooka.cinderella') is None)
|
| 793 |
+
bot.fail_send = None
|
| 794 |
+
|
| 795 |
+
# 9) زرّ التجربة من اللوحة: يُرسل دائمًا ولا يلوّث مانع التكرار
|
| 796 |
+
check('زرّ التجربة يُرسل بلا بثّ فعلي',
|
| 797 |
+
core.notify_live_start('example', {'nickname': 'مثال'}, test=True) is True)
|
| 798 |
+
tt = texts(bot)[-1]
|
| 799 |
+
check('رسالة التجربة مُعلَّمة كتجربة', 'تجريبية' in tt, tt)
|
| 800 |
+
check('التجربة لا تُسجَّل في مانع التكرار',
|
| 801 |
+
'example' not in (core.journal.read().get('notified') or {}))
|
| 802 |
+
|
| 803 |
+
# 10) قالب مخصّص + مفتاح غير معروف يبقى كما هو
|
| 804 |
+
os.environ['LIVE_NOTIFY_TEXT'] = '{name} مباشر {date} {time} {unknown_key}'
|
| 805 |
+
t4 = core.live_start_text('u', {'nickname': 'ن'}, when=datetime(2026, 1, 2, 3, 4))
|
| 806 |
+
check('القالب المخصّص مستعمل', t4.startswith('ن مباشر 02-01-2026 03:04'), t4)
|
| 807 |
+
check('مفتاح غير معروف لا يُسقط الرسالة', '{unknown_key}' in t4, t4)
|
| 808 |
+
os.environ['LIVE_NOTIFY_TEXT'] = ''
|
| 809 |
+
|
| 810 |
+
# 11) مانع التكرار لا يكبر بلا حدّ
|
| 811 |
+
core.journal.change(lambda d: d.update(notified={('k%d' % i): str(i) for i in range(250)}))
|
| 812 |
+
core.notify_live_start('capuser', {'room_id': 'R'})
|
| 813 |
+
check('مانع التكرار يبقى تحت 200 غرفة',
|
| 814 |
+
len(core.journal.read().get('notified') or {}) <= 200,
|
| 815 |
+
str(len(core.journal.read().get('notified') or {})))
|
| 816 |
+
check('أحدث الغرف محفوظة بعد التقليم',
|
| 817 |
+
(core.journal.read().get('notified') or {}).get('capuser') == 'R')
|
| 818 |
+
|
| 819 |
+
# 12) لقطة اللوحة تُظهر حالة التنبيه
|
| 820 |
+
snap = core.snapshot()
|
| 821 |
+
check('اللقطة تُظهر التنبيه مفعّلًا', snap.get('live_notify') is True)
|
| 822 |
+
check('اللقطة تُظهر وجهتي التنبيه', len(snap.get('live_notify_targets') or []) == 2,
|
| 823 |
+
str(snap.get('live_notify_targets')))
|
| 824 |
+
|
| 825 |
# --------------------------------------------------------------------------- #
|
| 826 |
failed = [r for r in RESULTS if not r[1]]
|
| 827 |
print('\n' + '=' * 62)
|
variables.env
CHANGED
|
@@ -46,6 +46,13 @@ TELEGRAM_REQUIRE_BACKUP=on
|
|
| 46 |
TELEGRAM_SILENT=off
|
| 47 |
# حارس المعدل: أقل فاصلة بين إرسالين للمحادثة نفسها (ثوانٍ)
|
| 48 |
TELEGRAM_MIN_CHAT_INTERVAL=4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
TELEGRAM_LOCAL_PORT=8081
|
| 50 |
|
| 51 |
# ---------- المراقبة والتسجيل والتخزين ----------
|
|
|
|
| 46 |
TELEGRAM_SILENT=off
|
| 47 |
# حارس المعدل: أقل فاصلة بين إرسالين للمحادثة نفسها (ثوانٍ)
|
| 48 |
TELEGRAM_MIN_CHAT_INTERVAL=4
|
| 49 |
+
# تنبيه «بدأ بثًّا مباشرًا» باسم المذيع والتاريخ والوقت — مرة لكل بثّ
|
| 50 |
+
# on/off. الإيقاف لا يمسّ التسجيل ولا الأرشفة.
|
| 51 |
+
LIVE_NOTIFY=on
|
| 52 |
+
# إلى أين: both = القناة + الحساب الشخصي | channel | backup
|
| 53 |
+
LIVE_NOTIFY_TARGETS=both
|
| 54 |
+
# قالب اختياري: {name} {username} {nickname} {live_title} {date} {time} {room_id}
|
| 55 |
+
LIVE_NOTIFY_TEXT=
|
| 56 |
TELEGRAM_LOCAL_PORT=8081
|
| 57 |
|
| 58 |
# ---------- المراقبة والتسجيل والتخزين ----------
|