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"{level_icon} تنبيه من بوت سناب شات\n\n" f"نوع المشكلة: {error_type}\n" f"المستوى: {level_label}\n" f"عدد مرات التكرار: {count}\n" f"الوقت: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n\n" f"التفاصيل:\n
{message[:800]}
\n\n" f"يتم مراقبة: {SNAPCHAT_USER}" ) 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"منشور جديد من {SNAPCHAT_USER}\n\n" f"مشاهدة الحساب على سناب شات" ) 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"[INFO] تم تشغيل البوت بنجاح\n\n" f"يتم الان مراقبة حساب: {SNAPCHAT_USER}\n" f"فترة الفحص: كل {CHECK_INTERVAL // 60} دقيقة\n" f"عتبة تنبيه الخطأ: بعد {ERROR_ALERT_THRESHOLD} تكرارات\n" f"الوقت: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}" ) 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 # ========================================== @app.route('/') def home(): return f""" Snap2Tele Bot Status

البوت يعمل بنجاح

ONLINE

يتم مراقبة حساب: {SNAPCHAT_USER}

فترة الفحص: كل {CHECK_INTERVAL // 60} دقيقة

عتبة تنبيه الاخطاء: {ERROR_ALERT_THRESHOLD} تكرارات

فترة التبريد بين التنبيهات: {ERROR_ALERT_COOLDOWN // 60} دقيقة



لإبقاء البوت يعمل 24/7 مجاناً، أضف هذا الرابط إلى cron-job.org

""" @app.route('/health') 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)