Update bot.py
Browse files
bot.py
CHANGED
|
@@ -47,7 +47,7 @@ CHANNELS_TO_SCRAPE = os.environ.get("CHANNELS_TO_SCRAPE", "")
|
|
| 47 |
|
| 48 |
DATA_DIR = "/data" if os.path.exists("/data") else "/tmp/data"
|
| 49 |
DOWNLOAD_DIR = os.path.join(DATA_DIR, "downloads")
|
| 50 |
-
DB_PATH = os.path.join(DATA_DIR, "
|
| 51 |
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
| 52 |
|
| 53 |
# ============ إنشاء العميل ============
|
|
@@ -102,31 +102,80 @@ class RateLimiter:
|
|
| 102 |
|
| 103 |
rate_limiter = RateLimiter(delay=2.0)
|
| 104 |
|
| 105 |
-
# ============ قاعدة البيانات ============
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
async def init_db():
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
async def add_message_to_queue(message, album_info=None):
|
| 132 |
if not message.media:
|
|
@@ -162,8 +211,8 @@ async def add_message_to_queue(message, album_info=None):
|
|
| 162 |
album_total = album_info.get("total", 1)
|
| 163 |
album_index = album_info.get("index", 1)
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
await db.execute("""
|
| 168 |
INSERT OR IGNORE INTO jobs
|
| 169 |
(chat_id, message_id, media_group_id, media_type, caption, source_name, album_total, album_index, status)
|
|
@@ -172,9 +221,11 @@ async def add_message_to_queue(message, album_info=None):
|
|
| 172 |
await db.commit()
|
| 173 |
stats["total_queued"] += 1
|
| 174 |
return True
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
| 178 |
|
| 179 |
# ============ إعداد الكابشن ============
|
| 180 |
def format_caption(raw_cap, source_name, album_index, album_total):
|
|
@@ -202,7 +253,7 @@ async def process_single_job(job):
|
|
| 202 |
|
| 203 |
await rate_limiter.wait()
|
| 204 |
|
| 205 |
-
# محاولة النسخ المباشر
|
| 206 |
try:
|
| 207 |
await app.copy_message(
|
| 208 |
chat_id=TARGET_CHAT,
|
|
@@ -224,7 +275,7 @@ async def process_single_job(job):
|
|
| 224 |
except Exception as e:
|
| 225 |
logger.warning(f"Direct copy failed for {msg_id} ({e}), falling back to download & upload...")
|
| 226 |
|
| 227 |
-
# التحميل والرفع في حال الحماية
|
| 228 |
try:
|
| 229 |
msg = await app.get_messages(chat_id, msg_id)
|
| 230 |
if not msg or not msg.media:
|
|
@@ -276,39 +327,52 @@ async def queue_worker():
|
|
| 276 |
await asyncio.sleep(5)
|
| 277 |
continue
|
| 278 |
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
await asyncio.sleep(2)
|
| 290 |
continue
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
job_id = job["id"]
|
| 294 |
-
|
| 295 |
-
await db.execute("UPDATE jobs SET status='processing' WHERE id=?", (job_id,))
|
| 296 |
-
await db.commit()
|
| 297 |
|
| 298 |
result = await process_single_job(job)
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
except (FloodWait, FloodPremiumWait) as e:
|
| 314 |
wait_val = getattr(e, 'value', 30)
|
|
|
|
| 47 |
|
| 48 |
DATA_DIR = "/data" if os.path.exists("/data") else "/tmp/data"
|
| 49 |
DOWNLOAD_DIR = os.path.join(DATA_DIR, "downloads")
|
| 50 |
+
DB_PATH = os.path.join(DATA_DIR, "archiver_queue.db")
|
| 51 |
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
| 52 |
|
| 53 |
# ============ إنشاء العميل ============
|
|
|
|
| 102 |
|
| 103 |
rate_limiter = RateLimiter(delay=2.0)
|
| 104 |
|
| 105 |
+
# ============ المعالجة التلقائية لقاعدة البيانات (Self-Healing DB) ============
|
| 106 |
+
def cleanup_corrupt_db():
|
| 107 |
+
logger.warning("Cleaning up corrupted database files...")
|
| 108 |
+
for ext in ["", "-wal", "-shm", "-journal"]:
|
| 109 |
+
file_to_del = f"{DB_PATH}{ext}"
|
| 110 |
+
if os.path.exists(file_to_del):
|
| 111 |
+
try:
|
| 112 |
+
os.remove(file_to_del)
|
| 113 |
+
logger.info(f"Deleted: {file_to_del}")
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Failed to delete {file_to_del}: {e}")
|
| 116 |
+
|
| 117 |
async def init_db():
|
| 118 |
+
try:
|
| 119 |
+
# فحص سلامة الملف إذا كان موجوداً
|
| 120 |
+
if os.path.exists(DB_PATH):
|
| 121 |
+
try:
|
| 122 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 123 |
+
cursor = await db.execute("PRAGMA integrity_check;")
|
| 124 |
+
res = await cursor.fetchone()
|
| 125 |
+
if not res or res[0] != "ok":
|
| 126 |
+
raise Exception("Integrity check failed")
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.error(f"Detected corrupt database ({e}), resetting...")
|
| 129 |
+
cleanup_corrupt_db()
|
| 130 |
+
|
| 131 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 132 |
+
await db.execute("PRAGMA journal_mode=WAL;")
|
| 133 |
+
await db.execute("PRAGMA synchronous=NORMAL;")
|
| 134 |
+
await db.execute("""
|
| 135 |
+
CREATE TABLE IF NOT EXISTS jobs (
|
| 136 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 137 |
+
chat_id INTEGER NOT NULL,
|
| 138 |
+
message_id INTEGER NOT NULL,
|
| 139 |
+
media_group_id TEXT,
|
| 140 |
+
media_type TEXT,
|
| 141 |
+
caption TEXT,
|
| 142 |
+
source_name TEXT,
|
| 143 |
+
album_total INTEGER DEFAULT 1,
|
| 144 |
+
album_index INTEGER DEFAULT 1,
|
| 145 |
+
status TEXT DEFAULT 'pending',
|
| 146 |
+
attempts INTEGER DEFAULT 0,
|
| 147 |
+
error TEXT,
|
| 148 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 149 |
+
UNIQUE(chat_id, message_id)
|
| 150 |
+
)
|
| 151 |
+
""")
|
| 152 |
+
await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
|
| 153 |
+
await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_media_group ON jobs(media_group_id)")
|
| 154 |
+
await db.commit()
|
| 155 |
+
logger.info("Database initialized successfully.")
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.error(f"Critical error initializing DB: {e}, recreating fresh DB...")
|
| 158 |
+
cleanup_corrupt_db()
|
| 159 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 160 |
+
await db.execute("""
|
| 161 |
+
CREATE TABLE IF NOT EXISTS jobs (
|
| 162 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 163 |
+
chat_id INTEGER NOT NULL,
|
| 164 |
+
message_id INTEGER NOT NULL,
|
| 165 |
+
media_group_id TEXT,
|
| 166 |
+
media_type TEXT,
|
| 167 |
+
caption TEXT,
|
| 168 |
+
source_name TEXT,
|
| 169 |
+
album_total INTEGER DEFAULT 1,
|
| 170 |
+
album_index INTEGER DEFAULT 1,
|
| 171 |
+
status TEXT DEFAULT 'pending',
|
| 172 |
+
attempts INTEGER DEFAULT 0,
|
| 173 |
+
error TEXT,
|
| 174 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 175 |
+
UNIQUE(chat_id, message_id)
|
| 176 |
+
)
|
| 177 |
+
""")
|
| 178 |
+
await db.commit()
|
| 179 |
|
| 180 |
async def add_message_to_queue(message, album_info=None):
|
| 181 |
if not message.media:
|
|
|
|
| 211 |
album_total = album_info.get("total", 1)
|
| 212 |
album_index = album_info.get("index", 1)
|
| 213 |
|
| 214 |
+
try:
|
| 215 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 216 |
await db.execute("""
|
| 217 |
INSERT OR IGNORE INTO jobs
|
| 218 |
(chat_id, message_id, media_group_id, media_type, caption, source_name, album_total, album_index, status)
|
|
|
|
| 221 |
await db.commit()
|
| 222 |
stats["total_queued"] += 1
|
| 223 |
return True
|
| 224 |
+
except Exception as e:
|
| 225 |
+
logger.error(f"Error adding message to DB queue: {e}")
|
| 226 |
+
if "malformed" in str(e).lower():
|
| 227 |
+
await init_db()
|
| 228 |
+
return False
|
| 229 |
|
| 230 |
# ============ إعداد الكابشن ============
|
| 231 |
def format_caption(raw_cap, source_name, album_index, album_total):
|
|
|
|
| 253 |
|
| 254 |
await rate_limiter.wait()
|
| 255 |
|
| 256 |
+
# 1. محاولة النسخ المباشر من السيرفر
|
| 257 |
try:
|
| 258 |
await app.copy_message(
|
| 259 |
chat_id=TARGET_CHAT,
|
|
|
|
| 275 |
except Exception as e:
|
| 276 |
logger.warning(f"Direct copy failed for {msg_id} ({e}), falling back to download & upload...")
|
| 277 |
|
| 278 |
+
# 2. التحميل والرفع في حال الحماية
|
| 279 |
try:
|
| 280 |
msg = await app.get_messages(chat_id, msg_id)
|
| 281 |
if not msg or not msg.media:
|
|
|
|
| 327 |
await asyncio.sleep(5)
|
| 328 |
continue
|
| 329 |
|
| 330 |
+
job = None
|
| 331 |
+
try:
|
| 332 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 333 |
+
db.row_factory = aiosqlite.Row
|
| 334 |
+
cursor = await db.execute("""
|
| 335 |
+
SELECT * FROM jobs
|
| 336 |
+
WHERE status = 'pending' AND attempts < 5
|
| 337 |
+
ORDER BY id ASC LIMIT 1
|
| 338 |
+
""")
|
| 339 |
+
job_row = await cursor.fetchone()
|
| 340 |
+
|
| 341 |
+
if not job_row:
|
| 342 |
+
await asyncio.sleep(2)
|
| 343 |
+
continue
|
| 344 |
+
|
| 345 |
+
job = dict(job_row)
|
| 346 |
+
job_id = job["id"]
|
| 347 |
+
|
| 348 |
+
await db.execute("UPDATE jobs SET status='processing' WHERE id=?", (job_id,))
|
| 349 |
+
await db.commit()
|
| 350 |
+
except Exception as db_err:
|
| 351 |
+
if "malformed" in str(db_err).lower():
|
| 352 |
+
logger.error("Database malformed in worker, resetting database...")
|
| 353 |
+
await init_db()
|
| 354 |
await asyncio.sleep(2)
|
| 355 |
continue
|
| 356 |
+
else:
|
| 357 |
+
raise db_err
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
|
| 359 |
result = await process_single_job(job)
|
| 360 |
|
| 361 |
+
try:
|
| 362 |
+
async with aiosqlite.connect(DB_PATH) as db:
|
| 363 |
+
if result is True:
|
| 364 |
+
await db.execute("UPDATE jobs SET status='completed' WHERE id=?", (job["id"],))
|
| 365 |
+
stats["total_transferred"] += 1
|
| 366 |
+
elif result == "DELETED":
|
| 367 |
+
await db.execute("UPDATE jobs SET status='failed', error='Message Deleted/Invalid' WHERE id=?", (job["id"],))
|
| 368 |
+
stats["total_failed"] += 1
|
| 369 |
+
else:
|
| 370 |
+
await db.execute("""
|
| 371 |
+
UPDATE jobs SET status='pending', attempts=attempts+1, error='Retry later' WHERE id=?
|
| 372 |
+
""", (job["id"],))
|
| 373 |
+
await db.commit()
|
| 374 |
+
except Exception as db_err:
|
| 375 |
+
logger.error(f"Failed to update job status: {db_err}")
|
| 376 |
|
| 377 |
except (FloodWait, FloodPremiumWait) as e:
|
| 378 |
wait_val = getattr(e, 'value', 30)
|