Download app.py from Reooiq/snap-max: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/Reooiq/snap-max/resolve/main/app.py
- Command line
-
hf download hf://spaces/Reooiq/snap-max/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Reooiq/snap-max/resolve/main/app.py
19.9 kB
| import os | |
| import time | |
| import json | |
| import logging | |
| import threading | |
| import requests | |
| from bs4 import BeautifulSoup | |
| from flask import Flask | |
| from functools import wraps | |
| # ========================================== | |
| # الإعدادات والمتغيرات (تدعم المتغيرات البيئية Secrets) | |
| # ========================================== | |
| SNAPCHAT_USER = os.environ.get("SNAPCHAT_USER", "a2001_89") | |
| TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "8272603814:AAHaqmdzUM1B4kG0Twtx3eKuk3PesPIvkfQ") | |
| TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "-1003584586221") | |
| TELEGRAM_BASE_URL = os.environ.get("TELEGRAM_BASE_URL", "https://api.telegram.org/") | |
| CHECK_INTERVAL = int(os.environ.get("CHECK_INTERVAL", 15)) * 60 | |
| POSTED_STORIES_FILE = "posted_stories.json" | |
| # ========================================== | |
| # [جديد] إعدادات نظام تنبيه الأخطاء | |
| # ========================================== | |
| # عدد المرات التي يجب أن يتكرر فيها الخطأ قبل إرسال تنبيه (لتجنب الفيضان) | |
| ERROR_ALERT_THRESHOLD = int(os.environ.get("ERROR_ALERT_THRESHOLD", 3)) | |
| # الحد الأدنى للوقت (بالثواني) بين كل تنبيه خطأ لنفس النوع (30 دقيقة افتراضياً) | |
| ERROR_ALERT_COOLDOWN = int(os.environ.get("ERROR_ALERT_COOLDOWN", 1800)) | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| app = Flask(__name__) | |
| # ========================================== | |
| # [جديد] مدير تنبيهات الأخطاء | |
| # ========================================== | |
| class ErrorAlertManager: | |
| """ | |
| يتتبع الأخطاء المتكررة ويرسل تنبيهاً عبر تليجرام | |
| فقط عند تجاوز الحد المسموح به، مع فترة تبريد لمنع الفيضان. | |
| """ | |
| def __init__(self): | |
| # يخزن: { "نوع_الخطأ": {"count": عدد, "last_alert_time": وقت} } | |
| self._error_registry = {} | |
| self._lock = threading.Lock() | |
| def report(self, error_type: str, message: str, critical: bool = False): | |
| """ | |
| يُسجّل خطأ. إذا تجاوز العتبة وانتهت فترة التبريد، يُرسل تنبيهاً لتليجرام. | |
| - error_type: معرف نصي للخطأ (مثل "snapchat_fetch_error") | |
| - message: تفاصيل الخطأ | |
| - critical: إذا True، يُرسَل التنبيه فوراً بغض النظر عن العتبة | |
| """ | |
| with self._lock: | |
| now = time.time() | |
| entry = self._error_registry.get(error_type, {"count": 0, "last_alert_time": 0}) | |
| entry["count"] += 1 | |
| self._error_registry[error_type] = entry | |
| should_alert = critical or (entry["count"] >= ERROR_ALERT_THRESHOLD) | |
| cooldown_passed = (now - entry["last_alert_time"]) >= ERROR_ALERT_COOLDOWN | |
| if should_alert and cooldown_passed: | |
| self._send_alert(error_type, message, entry["count"], critical) | |
| entry["count"] = 0 # إعادة تعيين العداد بعد الإرسال | |
| entry["last_alert_time"] = now | |
| self._error_registry[error_type] = entry | |
| def reset(self, error_type: str): | |
| """يُعيد تعيين عداد خطأ معين عند نجاح العملية""" | |
| with self._lock: | |
| if error_type in self._error_registry: | |
| self._error_registry[error_type]["count"] = 0 | |
| def _send_alert(self, error_type: str, message: str, count: int, critical: bool): | |
| """يرسل رسالة تنبيه مباشرة إلى تليجرام""" | |
| base_url = f"{TELEGRAM_BASE_URL.rstrip('/')}/bot{TELEGRAM_BOT_TOKEN}/" | |
| endpoint = base_url + "sendMessage" | |
| level_icon = "[CRITICAL]" if critical else "[WARNING]" | |
| level_label = "خطا حرج" if critical else "تحذير" | |
| text = ( | |
| f"<b>{level_icon} تنبيه من بوت سناب شات</b>\n\n" | |
| f"<b>نوع المشكلة:</b> <code>{error_type}</code>\n" | |
| f"<b>المستوى:</b> {level_label}\n" | |
| f"<b>عدد مرات التكرار:</b> {count}\n" | |
| f"<b>الوقت:</b> {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n\n" | |
| f"<b>التفاصيل:</b>\n<pre>{message[:800]}</pre>\n\n" | |
| f"<i>يتم مراقبة: {SNAPCHAT_USER}</i>" | |
| ) | |
| payload = { | |
| "chat_id": TELEGRAM_CHAT_ID, | |
| "text": text, | |
| "parse_mode": "HTML", | |
| "disable_web_page_preview": True | |
| } | |
| try: | |
| resp = requests.post(endpoint, json=payload, timeout=15) | |
| if resp.status_code == 200: | |
| logging.info(f"[AlertManager] تم إرسال تنبيه الخطأ: {error_type}") | |
| else: | |
| logging.warning(f"[AlertManager] فشل إرسال التنبيه: {resp.text}") | |
| except Exception as e: | |
| logging.error(f"[AlertManager] خطأ في إرسال التنبيه: {e}") | |
| # إنشاء مثيل عالمي مشترك | |
| alert_manager = ErrorAlertManager() | |
| # ========================================== | |
| # دوال إدارة السجل | |
| # ========================================== | |
| def load_posted_stories(): | |
| if os.path.exists(POSTED_STORIES_FILE): | |
| try: | |
| with open(POSTED_STORIES_FILE, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except Exception as e: | |
| msg = f"خطأ في قراءة ملف السجل: {e}" | |
| logging.error(msg) | |
| # [جديد] إبلاغ مدير التنبيهات | |
| alert_manager.report("log_file_read_error", msg) | |
| return [] | |
| def save_posted_stories(posted_list): | |
| try: | |
| with open(POSTED_STORIES_FILE, "w", encoding="utf-8") as f: | |
| json.dump(posted_list[-200:], f, ensure_ascii=False, indent=2) | |
| except Exception as e: | |
| msg = f"خطأ في حفظ ملف السجل: {e}" | |
| logging.error(msg) | |
| # [جديد] هذا خطأ حرج لأن البيانات قد تضيع | |
| alert_manager.report("log_file_write_error", msg, critical=True) | |
| # ========================================== | |
| # دالة الإرسال لتليجرام | |
| # ========================================== | |
| def send_to_telegram(media_url, is_video, caption): | |
| """تحميل المنشور الفعلي من سناب شات وإعادة رفعه كملف مباشر إلى التليجرام""" | |
| base_url = f"{TELEGRAM_BASE_URL.rstrip('/')}/bot{TELEGRAM_BOT_TOKEN}/" | |
| try: | |
| logging.info("جاري تنزيل ملف القصة من سناب شات...") | |
| headers = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/130.0.0.0 Safari/537.36" | |
| ) | |
| } | |
| media_response = requests.get(media_url, headers=headers, timeout=60) | |
| if media_response.status_code != 200: | |
| msg = f"فشل في تنزيل محتوى القصة. كود الحالة: {media_response.status_code} | الرابط: {media_url[:80]}" | |
| logging.error(msg) | |
| # [جديد] إبلاغ عن فشل التنزيل | |
| alert_manager.report("media_download_error", msg) | |
| return False | |
| # [جديد] إعادة تعيين عداد أخطاء التنزيل عند النجاح | |
| alert_manager.reset("media_download_error") | |
| content_type = media_response.headers.get('Content-Type', '') | |
| if 'video' in content_type.lower(): | |
| is_video = True | |
| files = {} | |
| data = { | |
| 'chat_id': TELEGRAM_CHAT_ID, | |
| 'caption': caption, | |
| 'parse_mode': 'HTML' | |
| } | |
| if is_video: | |
| endpoint = base_url + "sendVideo" | |
| files['video'] = ('story.mp4', media_response.content, 'video/mp4') | |
| logging.info("تم التعرف على الملف كـ (فيديو). جاري رفعه...") | |
| else: | |
| endpoint = base_url + "sendPhoto" | |
| files['photo'] = ('story.jpg', media_response.content, 'image/jpeg') | |
| logging.info("تم التعرف على الملف كـ (صورة). جاري الإرسال...") | |
| resp = requests.post(endpoint, data=data, files=files, timeout=120) | |
| if resp.status_code == 200: | |
| logging.info("تم إرسال القصة إلى تليجرام بنجاح!") | |
| # [جديد] إعادة تعيين عداد أخطاء تليجرام عند النجاح | |
| alert_manager.reset("telegram_send_error") | |
| return True | |
| else: | |
| msg = f"خطأ من تليجرام عند الإرسال: {resp.text[:300]}" | |
| logging.error(msg) | |
| # [جديد] إبلاغ عن فشل الإرسال لتليجرام | |
| alert_manager.report("telegram_send_error", msg) | |
| return False | |
| except requests.exceptions.Timeout: | |
| msg = f"انتهت مهلة الاتصال عند محاولة إرسال: {media_url[:80]}" | |
| logging.error(msg) | |
| alert_manager.report("send_timeout_error", msg) | |
| return False | |
| except Exception as e: | |
| msg = f"خطأ غير متوقع أثناء الإرسال: {e}" | |
| logging.error(msg) | |
| alert_manager.report("unexpected_send_error", msg, critical=True) | |
| return False | |
| # ========================================== | |
| # دالة استخراج قصص سناب شات | |
| # ========================================== | |
| def scrape_snapchat_stories(): | |
| """استخراج قصص سناب شات العامة من واجهة الويب""" | |
| url = f"https://www.snapchat.com/add/{SNAPCHAT_USER}" | |
| headers = { | |
| "User-Agent": ( | |
| "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " | |
| "AppleWebKit/537.36 (KHTML, like Gecko) " | |
| "Chrome/130.0.0.0 Safari/537.36" | |
| ), | |
| "Accept-Language": "en-US,en;q=0.9,ar;q=0.8" | |
| } | |
| try: | |
| logging.info(f"فحص الحساب: {SNAPCHAT_USER} ...") | |
| res = requests.get(url, headers=headers, timeout=20) | |
| if res.status_code == 404: | |
| msg = f"الحساب '{SNAPCHAT_USER}' غير موجود أو غير متاح للعامة." | |
| logging.warning(msg) | |
| # [جديد] هذا خطأ حرج لأنه قد يعني تغيير اسم المستخدم | |
| alert_manager.report("snapchat_account_not_found", msg, critical=True) | |
| return [] | |
| if res.status_code != 200: | |
| msg = f"فشل في الاتصال بسناب شات. كود الحالة: {res.status_code}" | |
| logging.error(msg) | |
| alert_manager.report("snapchat_fetch_error", msg) | |
| return [] | |
| # [جديد] إعادة تعيين عداد أخطاء الاتصال عند نجاح الجلب | |
| alert_manager.reset("snapchat_fetch_error") | |
| soup = BeautifulSoup(res.text, 'html.parser') | |
| script_data = soup.find('script', id='__NEXT_DATA__') | |
| if not script_data: | |
| for tag in soup.find_all('script', {'type': 'application/json'}): | |
| if 'mediaUrl' in tag.text or 'snapList' in tag.text or 'publicStories' in tag.text: | |
| script_data = tag | |
| break | |
| if not script_data: | |
| msg = "لم يتم العثور على بيانات القصص في كود الصفحة (قد يكون سناب شات غيّر بنية الصفحة)." | |
| logging.warning(msg) | |
| alert_manager.report("snapchat_parse_error", msg) | |
| return [] | |
| try: | |
| json_content = json.loads(script_data.string) | |
| except Exception as e: | |
| msg = f"خطأ في قراءة نص الـ JSON: {e}" | |
| logging.error(msg) | |
| alert_manager.report("json_parse_error", msg) | |
| return [] | |
| stories = [] | |
| seen_urls = set() | |
| def extract_snaps_recursively(obj): | |
| if isinstance(obj, dict): | |
| media_url = obj.get('mediaUrl') or obj.get('url') | |
| if not media_url and isinstance(obj.get('snapUrls'), dict): | |
| media_url = obj['snapUrls'].get('mediaUrl') | |
| video_url = obj.get('videoUrl') or obj.get('playbackUrl') or obj.get('defaultMediaUrl') | |
| if video_url and isinstance(video_url, str) and ('.mp4' in video_url or 'video' in video_url): | |
| media_url = video_url | |
| if media_url and isinstance(media_url, str) and media_url.startswith('http'): | |
| if 'profile' not in media_url.lower() and 'avatar' not in media_url.lower(): | |
| if media_url not in seen_urls: | |
| seen_urls.add(media_url) | |
| raw_id = obj.get('id') or obj.get('snapId') | |
| if isinstance(raw_id, dict) and 'value' in raw_id: | |
| snap_id = str(raw_id['value']) | |
| elif raw_id: | |
| snap_id = str(raw_id) | |
| else: | |
| snap_id = str(hash(media_url)) | |
| media_type = str(obj.get('mediaType', '')).upper() | |
| is_vid = 'VIDEO' in media_type or '.mp4' in media_url or 'video' in media_url | |
| if 'thumbnail' in media_url.lower() and not is_vid: | |
| pass | |
| else: | |
| timestamp = ( | |
| obj.get('timestampInSec') | |
| or obj.get('creationTimestamp') | |
| or int(time.time()) | |
| ) | |
| stories.append({ | |
| "id": str(snap_id), | |
| "url": media_url, | |
| "is_video": is_vid, | |
| "time": timestamp | |
| }) | |
| for val in obj.values(): | |
| extract_snaps_recursively(val) | |
| elif isinstance(obj, list): | |
| for item in obj: | |
| extract_snaps_recursively(item) | |
| try: | |
| extract_snaps_recursively(json_content) | |
| except Exception as ex: | |
| msg = f"خطأ غير متوقع أثناء الفحص التكراري للـ JSON: {ex}" | |
| logging.error(msg) | |
| alert_manager.report("recursive_parse_error", msg, critical=True) | |
| return stories | |
| except requests.exceptions.Timeout: | |
| msg = f"انتهت مهلة الاتصال بسناب شات (timeout) للحساب: {SNAPCHAT_USER}" | |
| logging.error(msg) | |
| alert_manager.report("snapchat_timeout_error", msg) | |
| return [] | |
| except Exception as e: | |
| msg = f"خطأ غير متوقع أثناء الاتصال بسناب شات: {e}" | |
| logging.error(msg) | |
| alert_manager.report("snapchat_fetch_error", msg, critical=True) | |
| return [] | |
| # ========================================== | |
| # الحلقة التكرارية للبوت | |
| # ========================================== | |
| def bot_worker(): | |
| """الحلقة التكرارية لعمل البوت في الخلفية""" | |
| logging.info("بدء تشغيل عامل البوت في الخلفية...") | |
| time.sleep(5) | |
| # [جديد] إرسال رسالة بدء التشغيل الأولي كتأكيد أن البوت يعمل | |
| _send_startup_notification() | |
| while True: | |
| posted_ids = load_posted_stories() | |
| current_stories = scrape_snapchat_stories() | |
| logging.info(f"تم العثور على {len(current_stories)} قصة عامة متاحة.") | |
| new_posted = False | |
| for story in current_stories: | |
| s_id = story["id"] | |
| if s_id not in posted_ids: | |
| logging.info(f"قصة جديدة تم رصدها! المعرف: {s_id[:10]}...") | |
| caption = ( | |
| f"منشور جديد من <b>{SNAPCHAT_USER}</b>\n\n" | |
| f"<a href='https://www.snapchat.com/add/{SNAPCHAT_USER}'>مشاهدة الحساب على سناب شات</a>" | |
| ) | |
| success = send_to_telegram(story["url"], story["is_video"], caption) | |
| if success: | |
| posted_ids.append(s_id) | |
| new_posted = True | |
| time.sleep(3) | |
| if new_posted: | |
| save_posted_stories(posted_ids) | |
| logging.info(f"الدخول في وضع الانتظار لمدة {CHECK_INTERVAL // 60} دقيقة...") | |
| time.sleep(CHECK_INTERVAL) | |
| def _send_startup_notification(): | |
| """[جديد] إرسال رسالة تأكيد تشغيل البوت عند بدء التشغيل""" | |
| base_url = f"{TELEGRAM_BASE_URL.rstrip('/')}/bot{TELEGRAM_BOT_TOKEN}/" | |
| endpoint = base_url + "sendMessage" | |
| text = ( | |
| f"<b>[INFO] تم تشغيل البوت بنجاح</b>\n\n" | |
| f"يتم الان مراقبة حساب: <b>{SNAPCHAT_USER}</b>\n" | |
| f"فترة الفحص: كل <b>{CHECK_INTERVAL // 60}</b> دقيقة\n" | |
| f"عتبة تنبيه الخطأ: بعد <b>{ERROR_ALERT_THRESHOLD}</b> تكرارات\n" | |
| f"<i>الوقت: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}</i>" | |
| ) | |
| try: | |
| requests.post(endpoint, json={ | |
| "chat_id": TELEGRAM_CHAT_ID, | |
| "text": text, | |
| "parse_mode": "HTML", | |
| "disable_web_page_preview": True | |
| }, timeout=15) | |
| except Exception as e: | |
| logging.warning(f"لم يتم إرسال رسالة بدء التشغيل: {e}") | |
| # ========================================== | |
| # خادم الويب للحفاظ على تشغيل Hugging Face | |
| # ========================================== | |
| def home(): | |
| return f""" | |
| <html> | |
| <head> | |
| <title>Snap2Tele Bot Status</title> | |
| <meta charset="utf-8"> | |
| <style> | |
| body {{ font-family: system-ui; text-align: center; padding: 50px; background: #f8fafc; color: #0f172a; }} | |
| .status {{ display: inline-block; padding: 10px 20px; background: #22c55e; color: white; border-radius: 50px; font-weight: bold; }} | |
| .info {{ margin-top: 20px; background: #e0f2fe; padding: 15px; border-radius: 10px; display: inline-block; text-align: left; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>البوت يعمل بنجاح</h1> | |
| <p class="status">ONLINE</p> | |
| <div class="info"> | |
| <p>يتم مراقبة حساب: <b>{SNAPCHAT_USER}</b></p> | |
| <p>فترة الفحص: كل <b>{CHECK_INTERVAL // 60}</b> دقيقة</p> | |
| <p>عتبة تنبيه الاخطاء: <b>{ERROR_ALERT_THRESHOLD}</b> تكرارات</p> | |
| <p>فترة التبريد بين التنبيهات: <b>{ERROR_ALERT_COOLDOWN // 60}</b> دقيقة</p> | |
| </div> | |
| <br><br> | |
| <p>لإبقاء البوت يعمل 24/7 مجاناً، أضف هذا الرابط إلى <b>cron-job.org</b></p> | |
| </body> | |
| </html> | |
| """ | |
| def health(): | |
| return { | |
| "status": "ok", | |
| "user": SNAPCHAT_USER, | |
| "timestamp": time.time(), | |
| "error_registry": { | |
| k: v["count"] | |
| for k, v in alert_manager._error_registry.items() | |
| } | |
| } | |
| if __name__ == '__main__': | |
| worker_thread = threading.Thread(target=bot_worker, daemon=True) | |
| worker_thread.start() | |
| port = int(os.environ.get('PORT', 7860)) | |
| app.run(host='0.0.0.0', port=port) | |