Reooiq commited on
Commit
0f6824d
·
verified ·
1 Parent(s): a6f9ba5

Update bot.py

Browse files
Files changed (1) hide show
  1. bot.py +123 -43
bot.py CHANGED
@@ -12,7 +12,18 @@ from hydrogram.types import (
12
  InputMediaAudio
13
  )
14
  from fastapi import FastAPI
15
- from tenacity import retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  logging.basicConfig(
18
  level=logging.INFO,
@@ -38,19 +49,42 @@ app = Client(
38
  api_hash=API_HASH,
39
  session_string=SESSION_STRING,
40
  in_memory=True,
41
- workers=2, # Hydrogram يتحمل workers أعلى
42
  device_model="Samsung Galaxy S23 Ultra",
43
  system_version="Android 14",
44
  app_version="10.14.1",
45
  lang_code="ar",
46
- takeout=False, # 🔑 مهم: لا يستخدم Takeout (يسبب مشاكل)
47
  )
48
 
49
  TARGET_CHAT = None
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  # ============ Throttle Controller ============
52
  class ThrottleController:
53
- def __init__(self, rate=2.0):
54
  self.rate = rate
55
  self.last_send = {}
56
  self.lock = asyncio.Lock()
@@ -64,11 +98,11 @@ class ThrottleController:
64
  await asyncio.sleep(self.rate - elapsed)
65
  self.last_send[chat_id] = time.time()
66
 
67
- throttle = ThrottleController(rate=2.0)
68
 
69
  # ============ Locks ============
70
  download_lock = asyncio.Lock()
71
- send_sem = asyncio.Semaphore(2)
72
 
73
  # ============ قاعدة البيانات ============
74
  async def init_db():
@@ -133,6 +167,14 @@ async def add_job(message):
133
  file_id = message.animation.file_id
134
  file_size = message.animation.file_size or 0
135
 
 
 
 
 
 
 
 
 
136
  src_name = get_source_name(message)
137
  raw_cap = message.caption or ""
138
  final_cap = f"{raw_cap}\n\nSource: {src_name}".strip() if raw_cap else f"Source: {src_name}"
@@ -144,7 +186,7 @@ async def add_job(message):
144
  (chat_id, message_id, media_group_id, media_type, file_id, file_name, file_size, caption, source_name, status)
145
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
146
  """, (
147
- message.chat.id, message.id, str(message.media_group_id) if message.media_group_id else None,
148
  m_type, file_id, file_name, file_size, final_cap, src_name
149
  ))
150
  await db.commit()
@@ -152,17 +194,21 @@ async def add_job(message):
152
  logger.error(f"Add job error: {e}")
153
 
154
  async def update_job_status(chat_id, message_id, status, file_path=None, error=None):
 
 
 
 
155
  async with aiosqlite.connect(DB_PATH) as db:
156
  if file_path:
157
  await db.execute("""
158
  UPDATE jobs SET status=?, file_path=?, updated_at=CURRENT_TIMESTAMP
159
  WHERE chat_id=? AND message_id=?
160
- """, (status, file_path, chat_id, message_id))
161
  elif error:
162
  await db.execute("""
163
  UPDATE jobs SET status=?, error=?, attempts=attempts+1, updated_at=CURRENT_TIMESTAMP
164
  WHERE chat_id=? AND message_id=?
165
- """, (status, error, chat_id, message_id))
166
  else:
167
  await db.execute("""
168
  UPDATE jobs SET status=?, updated_at=CURRENT_TIMESTAMP
@@ -178,7 +224,7 @@ async def get_pending_jobs(limit=1):
178
  WHERE status='pending' AND attempts < 10
179
  ORDER BY created_at
180
  LIMIT ?
181
- """, (limit,))
182
  rows = await cursor.fetchall()
183
  return [dict(row) for row in rows]
184
 
@@ -189,7 +235,7 @@ async def get_downloaded_album_jobs(media_group_id):
189
  SELECT * FROM jobs
190
  WHERE media_group_id=? AND status='downloaded'
191
  ORDER BY message_id
192
- """, (media_group_id,))
193
  rows = await cursor.fetchall()
194
  return [dict(row) for row in rows]
195
 
@@ -198,11 +244,13 @@ async def get_album_pending_count(media_group_id):
198
  cursor = await db.execute("""
199
  SELECT COUNT(*) FROM jobs
200
  WHERE media_group_id=? AND status IN ('pending', 'downloading')
201
- """, (media_group_id,))
202
  row = await cursor.fetchone()
203
  return row[0]
204
 
205
  async def delete_job(chat_id, message_id):
 
 
206
  async with aiosqlite.connect(DB_PATH) as db:
207
  cursor = await db.execute("""
208
  SELECT file_path FROM jobs WHERE chat_id=? AND message_id=?
@@ -261,7 +309,7 @@ async def download_with_message_object(chat_id, message_id, file_path):
261
  """إعادة جلب الرسالة وتحميلها (الأكثر موثوقية)"""
262
  try:
263
  logger.info(f"Re-fetching message {chat_id}/{message_id}")
264
- message = await app.get_messages(chat_id, message_id)
265
  if not message or not message.media:
266
  raise Exception("Message not found or no media")
267
 
@@ -276,10 +324,11 @@ async def download_with_message_object(chat_id, message_id, file_path):
276
 
277
  async def download_job(job):
278
  """3 محاولات: file_id → message object → final retry"""
279
- chat_id = job['chat_id']
280
- message_id = job['message_id']
281
- expected_size = job['file_size'] or 0
282
- file_id = job['file_id']
 
283
 
284
  ext = {
285
  'photo': '.jpg', 'video': '.mp4', 'doc': '',
@@ -295,6 +344,9 @@ async def download_job(job):
295
  except:
296
  pass
297
 
 
 
 
298
  try:
299
  await update_job_status(chat_id, message_id, 'downloading')
300
  downloaded = None
@@ -304,6 +356,9 @@ async def download_job(job):
304
  logger.info(f"Attempt 1 (file_id): {message_id}")
305
  try:
306
  downloaded = await app.download_media(file_id, file_name=file_path)
 
 
 
307
  except Exception as e:
308
  logger.warning(f"file_id failed: {e}")
309
 
@@ -315,10 +370,13 @@ async def download_job(job):
315
  # المحاولة 3: إعادة أخيرة
316
  if not await verify_download(downloaded, expected_size):
317
  logger.info(f"Attempt 3 (retry): {message_id}")
318
- await asyncio.sleep(3)
319
  if file_id:
320
  try:
321
  downloaded = await app.download_media(file_id, file_name=file_path)
 
 
 
322
  except:
323
  pass
324
  if not await verify_download(downloaded, expected_size):
@@ -334,26 +392,39 @@ async def download_job(job):
334
  except FloodWait as e:
335
  logger.warning(f"FloodWait: {e.value}s")
336
  await update_job_status(chat_id, message_id, 'pending', error=f"FloodWait:{e.value}")
337
- await asyncio.sleep(min(e.value, 60))
338
  return False
339
  except Exception as e:
340
- await update_job_status(chat_id, message_id, 'pending', error=str(e))
341
- logger.error(f"Download failed {message_id}: {e}")
 
342
  return False
343
 
344
  # ============ الإرسال ============
345
- @retry(
346
- stop=stop_after_attempt(5),
347
- wait=wait_exponential_jitter(initial=1, max=60),
348
- retry=retry_if_exception_type((FloodWait, ConnectionError, TimeoutError)),
349
- reraise=True
350
- )
351
  async def safe_send(send_func):
352
- return await send_func()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  async def send_job(job):
355
- chat_id = job['chat_id']
356
- message_id = job['message_id']
357
  file_path = job['file_path']
358
 
359
  if not file_path or not os.path.exists(file_path):
@@ -361,6 +432,9 @@ async def send_job(job):
361
  await update_job_status(chat_id, message_id, 'pending', error="File not found")
362
  return False
363
 
 
 
 
364
  try:
365
  await throttle.wait(TARGET_CHAT)
366
 
@@ -401,7 +475,7 @@ async def send_album_group(media_group_id):
401
  logger.info(f"Album {media_group_id} waiting for {pending} files")
402
  return
403
 
404
- jobs.sort(key=lambda j: j['message_id'])
405
 
406
  album_inputs = []
407
  album_jobs = []
@@ -427,6 +501,8 @@ async def send_album_group(media_group_id):
427
  else:
428
  standalone_jobs.append(job)
429
 
 
 
430
  async with send_sem:
431
  chunk_size = 10
432
  for idx in range(0, len(album_inputs), chunk_size):
@@ -461,12 +537,14 @@ async def download_worker():
461
  try:
462
  if not app.is_connected:
463
  logger.warning("Not connected, waiting...")
464
- await asyncio.sleep(5)
465
  continue
466
 
 
 
467
  jobs = await get_pending_jobs(limit=1)
468
  if not jobs:
469
- await asyncio.sleep(1)
470
  continue
471
 
472
  job = jobs[0]
@@ -480,7 +558,7 @@ async def download_worker():
480
 
481
  except Exception as e:
482
  logger.error(f"Download Worker error: {e}")
483
- await asyncio.sleep(5)
484
 
485
  async def send_worker():
486
  logger.info("Send Worker started")
@@ -490,16 +568,18 @@ async def send_worker():
490
  try:
491
  if not app.is_connected:
492
  logger.warning("Not connected, waiting...")
493
- await asyncio.sleep(5)
494
  continue
495
 
 
 
496
  async with aiosqlite.connect(DB_PATH) as db:
497
  db.row_factory = aiosqlite.Row
498
 
499
  cursor = await db.execute("""
500
  SELECT * FROM jobs
501
  WHERE status='downloaded' AND (media_group_id IS NULL OR media_group_id='')
502
- ORDER BY created_at LIMIT 3
503
  """)
504
  single_jobs = [dict(row) for row in await cursor.fetchall()]
505
 
@@ -507,7 +587,7 @@ async def send_worker():
507
  SELECT media_group_id FROM jobs
508
  WHERE status='downloaded' AND media_group_id IS NOT NULL AND media_group_id!=''
509
  GROUP BY media_group_id
510
- LIMIT 2
511
  """)
512
  album_rows = await cursor.fetchall()
513
 
@@ -516,19 +596,19 @@ async def send_worker():
516
  break
517
  async with send_sem:
518
  await send_job(job)
519
- await asyncio.sleep(2)
520
 
521
  for (mg_id,) in album_rows:
522
  if not app.is_connected:
523
  break
524
  await send_album_group(mg_id)
525
- await asyncio.sleep(2)
526
 
527
- await asyncio.sleep(1)
528
 
529
  except Exception as e:
530
  logger.error(f"Send Worker error: {e}")
531
- await asyncio.sleep(5)
532
 
533
  # ============ Album Fetch ============
534
  async def fetch_full_album(message):
@@ -545,7 +625,7 @@ async def fetch_full_album(message):
545
  # ============ Handlers ============
546
  @app.on_message(filters.incoming & ~filters.me & filters.media)
547
  async def on_media(client, message):
548
- chat_id = message.chat.id
549
  if str(chat_id) == str(TARGET_CHAT):
550
  return
551
 
@@ -656,4 +736,4 @@ if __name__ == "__main__":
656
  try:
657
  loop.run_until_complete(app.stop())
658
  except:
659
- pass
 
12
  InputMediaAudio
13
  )
14
  from fastapi import FastAPI
15
+
16
+ # ============ CRITICAL FIX: Monkey-patch FloodPremiumWait ============
17
+ # Older Hydrogram versions don't have FloodPremiumWait. Telegram sends it → AttributeError → infinite crash loop.
18
+ import hydrogram.errors.rpc_error as _rpc_error
19
+ if "FLOOD_PREMIUM_WAIT" not in [k for d in _rpc_error.exceptions.values() for k in d.keys()]:
20
+ class FloodPremiumWait(FloodWait):
21
+ CODE = 420
22
+ NAME = "FLOOD_PREMIUM_WAIT"
23
+ setattr(hydrogram.errors, "FloodPremiumWait", FloodPremiumWait)
24
+ _rpc_error.exceptions.setdefault(420, {})["FLOOD_PREMIUM_WAIT"] = "FloodPremiumWait"
25
+ logging.getLogger(__name__).warning("Patched missing FloodPremiumWait into Hydrogram")
26
+ # ===================================================================
27
 
28
  logging.basicConfig(
29
  level=logging.INFO,
 
49
  api_hash=API_HASH,
50
  session_string=SESSION_STRING,
51
  in_memory=True,
52
+ workers=2,
53
  device_model="Samsung Galaxy S23 Ultra",
54
  system_version="Android 14",
55
  app_version="10.14.1",
56
  lang_code="ar",
57
+ takeout=False,
58
  )
59
 
60
  TARGET_CHAT = None
61
 
62
+ # ============ Global Flood Control ============
63
+ class GlobalFloodControl:
64
+ """Pause ALL workers when ANY worker hits a FloodWait."""
65
+ def __init__(self):
66
+ self.until = 0
67
+ self._lock = asyncio.Lock()
68
+
69
+ async def hit(self, seconds):
70
+ async with self._lock:
71
+ target = time.time() + seconds
72
+ if target > self.until:
73
+ self.until = target
74
+ logger.warning(f"Global flood control activated: {seconds}s")
75
+
76
+ async def wait(self):
77
+ async with self._lock:
78
+ remaining = self.until - time.time()
79
+ if remaining > 0:
80
+ logger.info(f"Global flood pause: {remaining:.0f}s remaining")
81
+ await asyncio.sleep(remaining)
82
+
83
+ flood_control = GlobalFloodControl()
84
+
85
  # ============ Throttle Controller ============
86
  class ThrottleController:
87
+ def __init__(self, rate=3.0): # slightly slower to avoid bans
88
  self.rate = rate
89
  self.last_send = {}
90
  self.lock = asyncio.Lock()
 
98
  await asyncio.sleep(self.rate - elapsed)
99
  self.last_send[chat_id] = time.time()
100
 
101
+ throttle = ThrottleController(rate=3.0)
102
 
103
  # ============ Locks ============
104
  download_lock = asyncio.Lock()
105
+ send_sem = asyncio.Semaphore(1) # reduce to 1 to be gentler on API
106
 
107
  # ============ قاعدة البيانات ============
108
  async def init_db():
 
167
  file_id = message.animation.file_id
168
  file_size = message.animation.file_size or 0
169
 
170
+ # CRITICAL FIX: Explicit typing to prevent "unsupported type" SQLite errors
171
+ chat_id = int(message.chat.id)
172
+ message_id = int(message.id)
173
+ file_size = int(file_size) if file_size else 0
174
+ file_id = str(file_id) if file_id else None
175
+ file_name = str(file_name) if file_name else None
176
+ media_group_id = str(message.media_group_id) if message.media_group_id else None
177
+
178
  src_name = get_source_name(message)
179
  raw_cap = message.caption or ""
180
  final_cap = f"{raw_cap}\n\nSource: {src_name}".strip() if raw_cap else f"Source: {src_name}"
 
186
  (chat_id, message_id, media_group_id, media_type, file_id, file_name, file_size, caption, source_name, status)
187
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
188
  """, (
189
+ chat_id, message_id, media_group_id,
190
  m_type, file_id, file_name, file_size, final_cap, src_name
191
  ))
192
  await db.commit()
 
194
  logger.error(f"Add job error: {e}")
195
 
196
  async def update_job_status(chat_id, message_id, status, file_path=None, error=None):
197
+ # CRITICAL FIX: Explicit int typing
198
+ chat_id = int(chat_id)
199
+ message_id = int(message_id)
200
+
201
  async with aiosqlite.connect(DB_PATH) as db:
202
  if file_path:
203
  await db.execute("""
204
  UPDATE jobs SET status=?, file_path=?, updated_at=CURRENT_TIMESTAMP
205
  WHERE chat_id=? AND message_id=?
206
+ """, (status, str(file_path), chat_id, message_id))
207
  elif error:
208
  await db.execute("""
209
  UPDATE jobs SET status=?, error=?, attempts=attempts+1, updated_at=CURRENT_TIMESTAMP
210
  WHERE chat_id=? AND message_id=?
211
+ """, (status, str(error)[:500], chat_id, message_id)) # cap error length
212
  else:
213
  await db.execute("""
214
  UPDATE jobs SET status=?, updated_at=CURRENT_TIMESTAMP
 
224
  WHERE status='pending' AND attempts < 10
225
  ORDER BY created_at
226
  LIMIT ?
227
+ """, (int(limit),))
228
  rows = await cursor.fetchall()
229
  return [dict(row) for row in rows]
230
 
 
235
  SELECT * FROM jobs
236
  WHERE media_group_id=? AND status='downloaded'
237
  ORDER BY message_id
238
+ """, (str(media_group_id),))
239
  rows = await cursor.fetchall()
240
  return [dict(row) for row in rows]
241
 
 
244
  cursor = await db.execute("""
245
  SELECT COUNT(*) FROM jobs
246
  WHERE media_group_id=? AND status IN ('pending', 'downloading')
247
+ """, (str(media_group_id),))
248
  row = await cursor.fetchone()
249
  return row[0]
250
 
251
  async def delete_job(chat_id, message_id):
252
+ chat_id = int(chat_id)
253
+ message_id = int(message_id)
254
  async with aiosqlite.connect(DB_PATH) as db:
255
  cursor = await db.execute("""
256
  SELECT file_path FROM jobs WHERE chat_id=? AND message_id=?
 
309
  """إعادة جلب الرسالة وتحميلها (الأكثر موثوقية)"""
310
  try:
311
  logger.info(f"Re-fetching message {chat_id}/{message_id}")
312
+ message = await app.get_messages(int(chat_id), int(message_id))
313
  if not message or not message.media:
314
  raise Exception("Message not found or no media")
315
 
 
324
 
325
  async def download_job(job):
326
  """3 محاولات: file_id → message object → final retry"""
327
+ # CRITICAL FIX: Explicit int typing to prevent SQLite binding errors
328
+ chat_id = int(job['chat_id'])
329
+ message_id = int(job['message_id'])
330
+ expected_size = int(job['file_size'] or 0)
331
+ file_id = str(job['file_id']) if job['file_id'] else None
332
 
333
  ext = {
334
  'photo': '.jpg', 'video': '.mp4', 'doc': '',
 
344
  except:
345
  pass
346
 
347
+ # Respect global flood waits before starting
348
+ await flood_control.wait()
349
+
350
  try:
351
  await update_job_status(chat_id, message_id, 'downloading')
352
  downloaded = None
 
356
  logger.info(f"Attempt 1 (file_id): {message_id}")
357
  try:
358
  downloaded = await app.download_media(file_id, file_name=file_path)
359
+ except FloodWait as e:
360
+ await flood_control.hit(e.value)
361
+ raise
362
  except Exception as e:
363
  logger.warning(f"file_id failed: {e}")
364
 
 
370
  # المحاولة 3: إعادة أخيرة
371
  if not await verify_download(downloaded, expected_size):
372
  logger.info(f"Attempt 3 (retry): {message_id}")
373
+ await asyncio.sleep(5)
374
  if file_id:
375
  try:
376
  downloaded = await app.download_media(file_id, file_name=file_path)
377
+ except FloodWait as e:
378
+ await flood_control.hit(e.value)
379
+ raise
380
  except:
381
  pass
382
  if not await verify_download(downloaded, expected_size):
 
392
  except FloodWait as e:
393
  logger.warning(f"FloodWait: {e.value}s")
394
  await update_job_status(chat_id, message_id, 'pending', error=f"FloodWait:{e.value}")
395
+ await flood_control.hit(e.value)
396
  return False
397
  except Exception as e:
398
+ err_str = str(e)
399
+ await update_job_status(chat_id, message_id, 'pending', error=err_str)
400
+ logger.error(f"Download failed {message_id}: {err_str}")
401
  return False
402
 
403
  # ============ الإرسال ============
 
 
 
 
 
 
404
  async def safe_send(send_func):
405
+ """Manual retry that RESPECTS FloodWait from Telegram."""
406
+ for attempt in range(5):
407
+ try:
408
+ return await send_func()
409
+ except FloodWait as e:
410
+ wait_time = getattr(e, 'value', getattr(e, 'x', 30))
411
+ logger.warning(f"FloodWait {wait_time}s on attempt {attempt+1}/5")
412
+ await flood_control.hit(wait_time)
413
+ await asyncio.sleep(min(wait_time, 300))
414
+ except (ConnectionError, TimeoutError) as e:
415
+ wait_time = min(2 ** attempt, 60)
416
+ logger.warning(f"Connection error, retry in {wait_time}s: {e}")
417
+ await asyncio.sleep(wait_time)
418
+ except Exception as e:
419
+ if attempt == 4:
420
+ raise
421
+ logger.warning(f"Send error (attempt {attempt+1}): {e}")
422
+ await asyncio.sleep(min(2 ** attempt, 30))
423
+ return None
424
 
425
  async def send_job(job):
426
+ chat_id = int(job['chat_id'])
427
+ message_id = int(job['message_id'])
428
  file_path = job['file_path']
429
 
430
  if not file_path or not os.path.exists(file_path):
 
432
  await update_job_status(chat_id, message_id, 'pending', error="File not found")
433
  return False
434
 
435
+ # Respect global flood waits
436
+ await flood_control.wait()
437
+
438
  try:
439
  await throttle.wait(TARGET_CHAT)
440
 
 
475
  logger.info(f"Album {media_group_id} waiting for {pending} files")
476
  return
477
 
478
+ jobs.sort(key=lambda j: int(j['message_id']))
479
 
480
  album_inputs = []
481
  album_jobs = []
 
501
  else:
502
  standalone_jobs.append(job)
503
 
504
+ await flood_control.wait()
505
+
506
  async with send_sem:
507
  chunk_size = 10
508
  for idx in range(0, len(album_inputs), chunk_size):
 
537
  try:
538
  if not app.is_connected:
539
  logger.warning("Not connected, waiting...")
540
+ await asyncio.sleep(10)
541
  continue
542
 
543
+ await flood_control.wait()
544
+
545
  jobs = await get_pending_jobs(limit=1)
546
  if not jobs:
547
+ await asyncio.sleep(2)
548
  continue
549
 
550
  job = jobs[0]
 
558
 
559
  except Exception as e:
560
  logger.error(f"Download Worker error: {e}")
561
+ await asyncio.sleep(10)
562
 
563
  async def send_worker():
564
  logger.info("Send Worker started")
 
568
  try:
569
  if not app.is_connected:
570
  logger.warning("Not connected, waiting...")
571
+ await asyncio.sleep(10)
572
  continue
573
 
574
+ await flood_control.wait()
575
+
576
  async with aiosqlite.connect(DB_PATH) as db:
577
  db.row_factory = aiosqlite.Row
578
 
579
  cursor = await db.execute("""
580
  SELECT * FROM jobs
581
  WHERE status='downloaded' AND (media_group_id IS NULL OR media_group_id='')
582
+ ORDER BY created_at LIMIT 2
583
  """)
584
  single_jobs = [dict(row) for row in await cursor.fetchall()]
585
 
 
587
  SELECT media_group_id FROM jobs
588
  WHERE status='downloaded' AND media_group_id IS NOT NULL AND media_group_id!=''
589
  GROUP BY media_group_id
590
+ LIMIT 1
591
  """)
592
  album_rows = await cursor.fetchall()
593
 
 
596
  break
597
  async with send_sem:
598
  await send_job(job)
599
+ await asyncio.sleep(3)
600
 
601
  for (mg_id,) in album_rows:
602
  if not app.is_connected:
603
  break
604
  await send_album_group(mg_id)
605
+ await asyncio.sleep(3)
606
 
607
+ await asyncio.sleep(2)
608
 
609
  except Exception as e:
610
  logger.error(f"Send Worker error: {e}")
611
+ await asyncio.sleep(10)
612
 
613
  # ============ Album Fetch ============
614
  async def fetch_full_album(message):
 
625
  # ============ Handlers ============
626
  @app.on_message(filters.incoming & ~filters.me & filters.media)
627
  async def on_media(client, message):
628
+ chat_id = int(message.chat.id)
629
  if str(chat_id) == str(TARGET_CHAT):
630
  return
631
 
 
736
  try:
737
  loop.run_until_complete(app.stop())
738
  except:
739
+ pass