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"""
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)