Reooiq commited on
Commit
95a770b
·
verified ·
1 Parent(s): c1d92bb

Update bot.py

Browse files
Files changed (1) hide show
  1. bot.py +336 -585
bot.py CHANGED
@@ -6,105 +6,87 @@ import threading
6
  import aiosqlite
7
  import gradio as gr
8
  from hydrogram import Client, filters
9
- from hydrogram.errors import FloodWait
10
  from hydrogram.types import (
11
  InputMediaPhoto, InputMediaVideo, InputMediaDocument,
12
  InputMediaAudio
13
  )
14
  from fastapi import FastAPI
 
15
 
16
- # ============ CRITICAL FIX: Monkey-patch FloodPremiumWait ============
17
- import hydrogram
18
- import hydrogram.errors.rpc_error as _rpc_error
19
-
20
- if "FLOOD_PREMIUM_WAIT" not in [k for d in _rpc_error.exceptions.values() for k in d.keys()]:
21
- class FloodPremiumWait(FloodWait):
22
- CODE = 420
23
- NAME = "FLOOD_PREMIUM_WAIT"
24
- setattr(hydrogram.errors, "FloodPremiumWait", FloodPremiumWait)
25
- _rpc_error.exceptions.setdefault(420, {})["FLOOD_PREMIUM_WAIT"] = "FloodPremiumWait"
26
- logging.getLogger(__name__).warning("Patched missing FloodPremiumWait into Hydrogram")
27
- # ===================================================================
28
-
29
  logging.basicConfig(
30
  level=logging.INFO,
31
  format='%(asctime)s - %(levelname)s - %(message)s'
32
  )
33
- logger = logging.getLogger(__name__)
34
 
35
- # ============ الإعدادات ============
36
  API_ID = int(os.environ.get("API_ID", 0))
37
  API_HASH = os.environ.get("API_HASH", "")
38
  SESSION_STRING = os.environ.get("SESSION_STRING", "")
39
  TARGET_CHAT_RAW = os.environ.get("TARGET_CHAT", "me")
 
 
40
 
41
- DATA_DIR = "/data"
42
  DOWNLOAD_DIR = os.path.join(DATA_DIR, "downloads")
43
- DB_PATH = os.path.join(DATA_DIR, "queue.db")
44
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
45
 
46
- # ============ العميل (Hydrogram) ============
47
  app = Client(
48
- "archiver",
49
  api_id=API_ID,
50
  api_hash=API_HASH,
51
  session_string=SESSION_STRING,
52
  in_memory=True,
53
- workers=2,
54
- device_model="Samsung Galaxy S23 Ultra",
55
- system_version="Android 14",
56
- app_version="10.14.1",
57
- lang_code="ar",
58
- takeout=False,
59
  )
60
 
61
- TARGET_CHAT = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- # ============ Global Flood Control ============
64
- class GlobalFloodControl:
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):
88
- self.rate = rate
89
- self.last_send = {}
90
- self.lock = asyncio.Lock()
91
-
92
- async def wait(self, chat_id):
93
  async with self.lock:
 
94
  now = time.time()
95
- last = self.last_send.get(chat_id, 0)
96
- elapsed = now - last
97
- if elapsed < self.rate:
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)
106
 
107
- # ============ قاعدة البيانات ============
108
  async def init_db():
109
  async with aiosqlite.connect(DB_PATH) as db:
110
  await db.execute("""
@@ -114,606 +96,377 @@ async def init_db():
114
  message_id INTEGER NOT NULL,
115
  media_group_id TEXT,
116
  media_type TEXT,
117
- file_id TEXT,
118
- file_name TEXT,
119
- file_size INTEGER DEFAULT 0,
120
- file_path TEXT,
121
  caption TEXT,
122
  source_name TEXT,
123
- status TEXT DEFAULT 'pending',
 
 
124
  attempts INTEGER DEFAULT 0,
125
  error TEXT,
126
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
127
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
128
  UNIQUE(chat_id, message_id)
129
  )
130
  """)
131
  await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
132
  await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_media_group ON jobs(media_group_id)")
133
  await db.commit()
134
- logger.info("Database ready")
135
 
136
- async def add_job(message):
137
- m_type, ext = get_media_info(message)
138
- if not m_type:
139
- return
140
-
141
- file_id = None
142
- file_size = 0
143
- file_name = None
144
-
145
- if message.photo:
146
- file_id = message.photo.file_id
147
- file_size = message.photo.file_size or 0
148
- elif message.video:
149
- file_id = message.video.file_id
150
- file_size = message.video.file_size or 0
151
- file_name = message.video.file_name
152
- elif message.document:
153
- file_id = message.document.file_id
154
- file_size = message.document.file_size or 0
155
- file_name = message.document.file_name
156
- elif message.audio:
157
- file_id = message.audio.file_id
158
- file_size = message.audio.file_size or 0
159
- file_name = message.audio.file_name
160
- elif message.voice:
161
- file_id = message.voice.file_id
162
- file_size = message.voice.file_size or 0
163
- elif message.video_note:
164
- file_id = message.video_note.file_id
165
- file_size = message.video_note.file_size or 0
166
- elif message.animation:
167
- file_id = message.animation.file_id
168
- file_size = message.animation.file_size or 0
169
 
170
  chat_id = int(message.chat.id)
171
  message_id = int(message.id)
172
- file_size = int(file_size) if file_size else 0
173
- file_id = str(file_id) if file_id else None
174
- file_name = str(file_name) if file_name else None
175
  media_group_id = str(message.media_group_id) if message.media_group_id else None
176
 
177
- src_name = get_source_name(message)
178
- raw_cap = message.caption or ""
179
- final_cap = f"{raw_cap}\n\nSource: {src_name}".strip() if raw_cap else f"Source: {src_name}"
 
 
 
 
 
 
 
 
 
 
 
 
180
 
 
 
 
 
 
 
 
181
  async with aiosqlite.connect(DB_PATH) as db:
182
  try:
183
  await db.execute("""
184
  INSERT OR IGNORE INTO jobs
185
- (chat_id, message_id, media_group_id, media_type, file_id, file_name, file_size, caption, source_name, status)
186
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
187
- """, (
188
- chat_id, message_id, media_group_id,
189
- m_type, file_id, file_name, file_size, final_cap, src_name
190
- ))
191
  await db.commit()
 
 
192
  except Exception as e:
193
- logger.error(f"Add job error: {e}")
194
-
195
- async def update_job_status(chat_id, message_id, status, file_path=None, error=None):
196
- chat_id = int(chat_id)
197
- message_id = int(message_id)
198
-
199
- async with aiosqlite.connect(DB_PATH) as db:
200
- if file_path:
201
- await db.execute("""
202
- UPDATE jobs SET status=?, file_path=?, updated_at=CURRENT_TIMESTAMP
203
- WHERE chat_id=? AND message_id=?
204
- """, (status, str(file_path), chat_id, message_id))
205
- elif error:
206
- await db.execute("""
207
- UPDATE jobs SET status=?, error=?, attempts=attempts+1, updated_at=CURRENT_TIMESTAMP
208
- WHERE chat_id=? AND message_id=?
209
- """, (status, str(error)[:500], chat_id, message_id))
210
- else:
211
- await db.execute("""
212
- UPDATE jobs SET status=?, updated_at=CURRENT_TIMESTAMP
213
- WHERE chat_id=? AND message_id=?
214
- """, (status, chat_id, message_id))
215
- await db.commit()
216
-
217
- async def get_pending_jobs(limit=1):
218
- async with aiosqlite.connect(DB_PATH) as db:
219
- db.row_factory = aiosqlite.Row
220
- cursor = await db.execute("""
221
- SELECT * FROM jobs
222
- WHERE status='pending' AND attempts < 10
223
- ORDER BY created_at
224
- LIMIT ?
225
- """, (int(limit),))
226
- rows = await cursor.fetchall()
227
- return [dict(row) for row in rows]
228
-
229
- async def get_downloaded_album_jobs(media_group_id):
230
- async with aiosqlite.connect(DB_PATH) as db:
231
- db.row_factory = aiosqlite.Row
232
- cursor = await db.execute("""
233
- SELECT * FROM jobs
234
- WHERE media_group_id=? AND status='downloaded'
235
- ORDER BY message_id
236
- """, (str(media_group_id),))
237
- rows = await cursor.fetchall()
238
- return [dict(row) for row in rows]
239
-
240
- async def get_album_pending_count(media_group_id):
241
- async with aiosqlite.connect(DB_PATH) as db:
242
- cursor = await db.execute("""
243
- SELECT COUNT(*) FROM jobs
244
- WHERE media_group_id=? AND status IN ('pending', 'downloading')
245
- """, (str(media_group_id),))
246
- row = await cursor.fetchone()
247
- return row[0]
248
-
249
- async def delete_job(chat_id, message_id):
250
- chat_id = int(chat_id)
251
- message_id = int(message_id)
252
- async with aiosqlite.connect(DB_PATH) as db:
253
- cursor = await db.execute("""
254
- SELECT file_path FROM jobs WHERE chat_id=? AND message_id=?
255
- """, (chat_id, message_id))
256
- row = await cursor.fetchone()
257
- if row and row[0] and os.path.exists(row[0]):
258
- try:
259
- os.remove(row[0])
260
- except:
261
- pass
262
- await db.execute("DELETE FROM jobs WHERE chat_id=? AND message_id=?", (chat_id, message_id))
263
- await db.commit()
264
-
265
- # ============ معلومات الملف ============
266
- def get_media_info(message):
267
- if message.photo:
268
- return "photo", ".jpg"
269
- elif message.video:
270
- return "video", ".mp4"
271
- elif message.document:
272
- ext = os.path.splitext(message.document.file_name or "")[1] or ""
273
- return "doc", ext
274
- elif message.audio:
275
- ext = os.path.splitext(message.audio.file_name or "")[1] or ".mp3"
276
- return "audio", ext
277
- elif message.voice:
278
- return "voice", ".ogg"
279
- elif message.video_note:
280
- return "video_note", ".mp4"
281
- elif message.animation:
282
- return "animation", ".mp4"
283
- return None, ""
284
-
285
- def get_source_name(message):
286
- chat = message.chat
287
- if chat.type in ["channel", "supergroup", "group"]:
288
- return chat.title or "Channel/Group"
289
- elif chat.type == "private":
290
- return chat.first_name or chat.username or "Private"
291
- return "Source"
292
-
293
- # ============ التحميل المُصلح ============
294
- async def verify_download(file_path, expected_size):
295
- if not file_path or not os.path.exists(file_path):
296
- return False
297
- actual_size = os.path.getsize(file_path)
298
- if expected_size > 0 and actual_size < expected_size:
299
- logger.warning(f"Incomplete file: {actual_size}/{expected_size} bytes")
300
- return False
301
- if actual_size == 0:
302
- logger.warning(f"Zero-byte file: {file_path}")
303
- return False
304
- return True
305
 
306
- async def download_with_message_object(chat_id, message_id, file_path):
307
- try:
308
- logger.info(f"Re-fetching message {chat_id}/{message_id}")
309
- message = await app.get_messages(int(chat_id), int(message_id))
310
- if not message or not message.media:
311
- raise Exception("Message not found or no media")
312
-
313
- downloaded = await app.download_media(
314
- message,
315
- file_name=file_path
316
- )
317
- return downloaded
318
- except Exception as e:
319
- logger.error(f"Re-fetch download failed: {e}")
320
- return None
321
-
322
- async def download_job(job):
323
- chat_id = int(job['chat_id'])
324
- message_id = int(job['message_id'])
325
- expected_size = int(job['file_size'] or 0)
326
- file_id = str(job['file_id']) if job['file_id'] else None
327
 
328
- ext = {
329
- 'photo': '.jpg', 'video': '.mp4', 'doc': '',
330
- 'audio': '.mp3', 'voice': '.ogg',
331
- 'video_note': '.mp4', 'animation': '.mp4'
332
- }.get(job['media_type'], '')
333
 
334
- file_path = os.path.join(DOWNLOAD_DIR, f"{chat_id}_{message_id}{ext}")
335
-
336
- if os.path.exists(file_path):
337
- try:
338
- os.remove(file_path)
339
- except:
340
- pass
 
 
 
341
 
342
- await flood_control.wait()
 
 
343
 
 
344
  try:
345
- await update_job_status(chat_id, message_id, 'downloading')
346
- downloaded = None
347
-
348
- if file_id:
349
- logger.info(f"Attempt 1 (file_id): {message_id}")
350
- try:
351
- downloaded = await app.download_media(file_id, file_name=file_path)
352
- except FloodWait as e:
353
- await flood_control.hit(e.value)
354
- raise
355
- except Exception as e:
356
- logger.warning(f"file_id failed: {e}")
357
-
358
- if not await verify_download(downloaded, expected_size):
359
- logger.info(f"Attempt 2 (message): {message_id}")
360
- downloaded = await download_with_message_object(chat_id, message_id, file_path)
361
-
362
- if not await verify_download(downloaded, expected_size):
363
- logger.info(f"Attempt 3 (retry): {message_id}")
364
- await asyncio.sleep(5)
365
- if file_id:
366
- try:
367
- downloaded = await app.download_media(file_id, file_name=file_path)
368
- except FloodWait as e:
369
- await flood_control.hit(e.value)
370
- raise
371
- except:
372
- pass
373
- if not await verify_download(downloaded, expected_size):
374
- downloaded = await download_with_message_object(chat_id, message_id, file_path)
375
-
376
- if not await verify_download(downloaded, expected_size):
377
- raise Exception(f"All attempts failed")
378
-
379
- await update_job_status(chat_id, message_id, 'downloaded', file_path=downloaded)
380
- logger.info(f"Download complete: {message_id} | {os.path.getsize(downloaded)} bytes")
381
  return True
382
-
383
- except FloodWait as e:
384
- logger.warning(f"FloodWait: {e.value}s")
385
- await update_job_status(chat_id, message_id, 'pending', error=f"FloodWait:{e.value}")
386
- await flood_control.hit(e.value)
387
- return False
 
 
388
  except Exception as e:
389
- err_str = str(e)
390
- await update_job_status(chat_id, message_id, 'pending', error=err_str)
391
- logger.error(f"Download failed {message_id}: {err_str}")
392
- return False
393
 
394
- # ============ الإرسال ============
395
- async def safe_send(send_func):
396
- for attempt in range(5):
397
- try:
398
- return await send_func()
399
- except FloodWait as e:
400
- wait_time = getattr(e, 'value', getattr(e, 'x', 30))
401
- logger.warning(f"FloodWait {wait_time}s on attempt {attempt+1}/5")
402
- await flood_control.hit(wait_time)
403
- await asyncio.sleep(min(wait_time, 300))
404
- except (ConnectionError, TimeoutError) as e:
405
- wait_time = min(2 ** attempt, 60)
406
- logger.warning(f"Connection error, retry in {wait_time}s: {e}")
407
- await asyncio.sleep(wait_time)
408
- except Exception as e:
409
- if attempt == 4:
410
- raise
411
- logger.warning(f"Send error (attempt {attempt+1}): {e}")
412
- await asyncio.sleep(min(2 ** attempt, 30))
413
- return None
414
-
415
- async def send_job(job):
416
- chat_id = int(job['chat_id'])
417
- message_id = int(job['message_id'])
418
- file_path = job['file_path']
419
-
420
- if not file_path or not os.path.exists(file_path):
421
- logger.error(f"File not found: {file_path}")
422
- await update_job_status(chat_id, message_id, 'pending', error="File not found")
423
- return False
424
-
425
- await flood_control.wait()
426
-
427
  try:
428
- await throttle.wait(TARGET_CHAT)
429
-
430
- m_type = job['media_type']
431
- caption = job['caption'] or ""
432
-
433
- if m_type == 'photo':
434
- await safe_send(lambda: app.send_photo(TARGET_CHAT, photo=file_path, caption=caption))
435
- elif m_type == 'video':
436
- await safe_send(lambda: app.send_video(TARGET_CHAT, video=file_path, caption=caption, supports_streaming=True))
437
- elif m_type == 'doc':
438
- await safe_send(lambda: app.send_document(TARGET_CHAT, document=file_path, caption=caption))
439
- elif m_type == 'audio':
440
- await safe_send(lambda: app.send_audio(TARGET_CHAT, audio=file_path, caption=caption))
441
- elif m_type == 'voice':
442
- await safe_send(lambda: app.send_voice(TARGET_CHAT, voice=file_path, caption=caption))
443
- elif m_type == 'video_note':
444
- await safe_send(lambda: app.send_video_note(TARGET_CHAT, video_note=file_path))
445
- elif m_type == 'animation':
446
- await safe_send(lambda: app.send_animation(TARGET_CHAT, animation=file_path, caption=caption))
447
 
448
- logger.info(f"Sent: {message_id}")
449
- await delete_job(chat_id, message_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  return True
451
-
 
 
452
  except Exception as e:
453
- logger.error(f"Send failed {message_id}: {e}")
454
- await update_job_status(chat_id, message_id, 'downloaded', error=str(e))
455
  return False
456
 
457
- async def send_album_group(media_group_id):
458
- jobs = await get_downloaded_album_jobs(media_group_id)
459
- if not jobs:
460
- return
461
-
462
- pending = await get_album_pending_count(media_group_id)
463
- if pending > 0:
464
- logger.info(f"Album {media_group_id} waiting for {pending} files")
465
- return
466
-
467
- jobs.sort(key=lambda j: int(j['message_id']))
468
-
469
- album_inputs = []
470
- album_jobs = []
471
- standalone_jobs = []
472
-
473
- for i, job in enumerate(jobs):
474
- cap = job['caption'] if i == 0 else None
475
- m_type = job['media_type']
476
- file_path = job['file_path']
477
-
478
- if m_type == 'photo':
479
- album_inputs.append(InputMediaPhoto(media=file_path, caption=cap))
480
- album_jobs.append(job)
481
- elif m_type == 'video':
482
- album_inputs.append(InputMediaVideo(media=file_path, caption=cap, supports_streaming=True))
483
- album_jobs.append(job)
484
- elif m_type == 'doc':
485
- album_inputs.append(InputMediaDocument(media=file_path, caption=cap))
486
- album_jobs.append(job)
487
- elif m_type == 'audio':
488
- album_inputs.append(InputMediaAudio(media=file_path, caption=cap))
489
- album_jobs.append(job)
490
- else:
491
- standalone_jobs.append(job)
492
-
493
- await flood_control.wait()
494
-
495
- async with send_sem:
496
- chunk_size = 10
497
- for idx in range(0, len(album_inputs), chunk_size):
498
- chunk = album_inputs[idx:idx+chunk_size]
499
- chunk_jobs = album_jobs[idx:idx+chunk_size]
500
-
501
- try:
502
- if len(chunk) == 1:
503
- await safe_send(lambda: app.send_photo(
504
- TARGET_CHAT, photo=chunk[0].media, caption=chunk[0].caption
505
- ))
506
- else:
507
- await safe_send(lambda: app.send_media_group(
508
- TARGET_CHAT, media=chunk, disable_notification=True
509
- ))
510
-
511
- for j in chunk_jobs:
512
- await delete_job(j['chat_id'], j['message_id'])
513
- logger.info(f"Album {media_group_id} part {idx//chunk_size + 1} sent")
514
- except Exception as e:
515
- logger.error(f"Album send failed: {e}")
516
-
517
- for job in standalone_jobs:
518
- await send_job(job)
519
-
520
- # ============ Workers ============
521
- async def download_worker():
522
- logger.info("Download Worker started")
523
- await asyncio.sleep(5)
524
-
525
  while True:
526
  try:
527
  if not app.is_connected:
528
- logger.warning("Not connected, waiting...")
529
- await asyncio.sleep(10)
530
  continue
531
-
532
- await flood_control.wait()
533
-
534
- jobs = await get_pending_jobs(limit=1)
535
- if not jobs:
536
- await asyncio.sleep(2)
537
- continue
538
-
539
- job = jobs[0]
540
-
541
- async with download_lock:
542
- success = await download_job(job)
543
- if success:
544
- await asyncio.sleep(3)
545
- else:
546
- await asyncio.sleep(5)
547
-
548
- except Exception as e:
549
- logger.error(f"Download Worker error: {e}")
550
- await asyncio.sleep(10)
551
 
552
- async def send_worker():
553
- logger.info("Send Worker started")
554
- await asyncio.sleep(7)
555
-
556
- while True:
557
- try:
558
- if not app.is_connected:
559
- logger.warning("Not connected, waiting...")
560
- await asyncio.sleep(10)
561
- continue
562
-
563
- await flood_control.wait()
564
-
565
  async with aiosqlite.connect(DB_PATH) as db:
566
  db.row_factory = aiosqlite.Row
567
-
568
  cursor = await db.execute("""
569
  SELECT * FROM jobs
570
- WHERE status='downloaded' AND (media_group_id IS NULL OR media_group_id='')
571
- ORDER BY created_at LIMIT 2
572
  """)
573
- single_jobs = [dict(row) for row in await cursor.fetchall()]
574
 
575
- cursor = await db.execute("""
576
- SELECT media_group_id FROM jobs
577
- WHERE status='downloaded' AND media_group_id IS NOT NULL AND media_group_id!=''
578
- GROUP BY media_group_id
579
- LIMIT 1
580
- """)
581
- album_rows = await cursor.fetchall()
582
-
583
- for job in single_jobs:
584
- if not app.is_connected:
585
- break
586
- async with send_sem:
587
- await send_job(job)
588
- await asyncio.sleep(3)
589
-
590
- for (mg_id,) in album_rows:
591
- if not app.is_connected:
592
- break
593
- await send_album_group(mg_id)
594
- await asyncio.sleep(3)
595
-
596
- await asyncio.sleep(2)
597
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  except Exception as e:
599
- logger.error(f"Send Worker error: {e}")
600
- await asyncio.sleep(10)
601
 
602
- # ============ Album Fetch ============
603
- async def fetch_full_album(message):
 
 
 
604
  try:
605
- messages = await app.get_media_group(message.chat.id, message.id)
606
- logger.info(f"get_media_group: {len(messages)} messages")
607
- for msg in messages:
608
- await add_job(msg)
609
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
  except Exception as e:
611
- logger.warning(f"get_media_group failed: {e}")
612
- return False
613
 
614
- # ============ Handlers ============
615
  @app.on_message(filters.incoming & ~filters.me & filters.media)
616
- async def on_media(client, message):
617
- chat_id = int(message.chat.id)
618
- if str(chat_id) == str(TARGET_CHAT):
619
- return
620
-
621
- src_name = get_source_name(message)
622
- m_type, _ = get_media_info(message)
623
- logger.info(f"New: {chat_id}/{message.id} | {m_type} | album: {message.media_group_id is not None}")
624
-
625
- if message.media_group_id:
626
- success = await fetch_full_album(message)
627
- if not success:
628
- await add_job(message)
629
- else:
630
- await add_job(message)
631
-
632
- @app.on_message(filters.incoming & ~filters.me)
633
- async def monitor_all(client, message):
634
- if str(message.chat.id) == str(TARGET_CHAT):
635
- return
636
- logger.info(f"Text from {message.chat.id}")
 
 
 
637
 
638
- async def resolve_target_chat():
 
639
  global TARGET_CHAT
640
- raw = TARGET_CHAT_RAW
641
- if raw.lower() == "me":
642
  TARGET_CHAT = "me"
 
643
  return
644
  try:
645
  chat_id_int = int(raw)
646
  chat = await app.get_chat(chat_id_int)
647
  TARGET_CHAT = chat_id_int
648
- logger.info(f"Target: {chat.title}")
649
- except Exception as e:
650
- logger.error(f"Target check failed: {e}")
651
- TARGET_CHAT = "me"
 
 
 
 
 
652
 
653
- # ============ Gradio + FastAPI ============
654
- def run_gradio():
655
  with gr.Blocks(title="Telegram Archiver Pro") as demo:
656
- gr.Markdown("# Telegram Archiver Pro")
657
- gr.Markdown("Status: Running | Hydrogram | Serial | Verified")
658
- gr.Textbox(label="Status", value="Active | SQLite Queue | 3-Attempt DL", interactive=False)
659
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  fastapi_app = FastAPI()
661
-
662
- @fastapi_app.get("/health")
663
- async def health_check():
664
- return {"status": "ok", "timestamp": time.time()}
665
 
666
  @fastapi_app.get("/ping")
667
  async def ping():
668
- return {"status": "alive"}
669
-
670
- gr.mount_gradio_app(fastapi_app, demo, path="/")
671
-
672
- import uvicorn
673
  uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
674
 
675
- # ============ Main ============
676
  async def main():
677
- try:
678
- if app.is_connected:
679
- await app.stop()
680
- await asyncio.sleep(2)
681
- except:
682
- pass
683
-
684
  await app.start()
685
 
686
- retries = 0
687
- while not app.is_connected and retries < 10:
688
- await asyncio.sleep(1)
689
- retries += 1
690
- logger.info(f"Waiting for connection... ({retries}/10)")
691
-
692
- if not app.is_connected:
693
- logger.error("Failed to connect")
694
- return
695
-
696
  me = await app.get_me()
697
- await resolve_target_chat()
 
 
698
  await init_db()
699
 
700
- await asyncio.sleep(2)
701
- asyncio.create_task(download_worker())
702
- await asyncio.sleep(2)
703
- asyncio.create_task(send_worker())
704
 
705
- logger.info("=" * 60)
706
- logger.info(f"Archiver Pro running!")
707
- logger.info(f"Account: {me.first_name}")
708
- logger.info(f"Target: {TARGET_CHAT}")
709
- logger.info("=" * 60)
 
 
710
 
 
711
  stop_event = asyncio.Event()
712
  await stop_event.wait()
713
 
714
  if __name__ == "__main__":
715
- gradio_thread = threading.Thread(target=run_gradio, daemon=True)
716
- gradio_thread.start()
 
717
 
718
  loop = asyncio.get_event_loop()
719
  try:
@@ -721,7 +474,5 @@ if __name__ == "__main__":
721
  except KeyboardInterrupt:
722
  pass
723
  finally:
724
- try:
725
- loop.run_until_complete(app.stop())
726
- except:
727
- pass
 
6
  import aiosqlite
7
  import gradio as gr
8
  from hydrogram import Client, filters
9
+ from hydrogram.errors import FloodWait, FloodPremiumWait, ChatForwardsRestricted, MessageIdInvalid
10
  from hydrogram.types import (
11
  InputMediaPhoto, InputMediaVideo, InputMediaDocument,
12
  InputMediaAudio
13
  )
14
  from fastapi import FastAPI
15
+ import uvicorn
16
 
17
+ # ============ إعداد السجلات (Logging) ============
 
 
 
 
 
 
 
 
 
 
 
 
18
  logging.basicConfig(
19
  level=logging.INFO,
20
  format='%(asctime)s - %(levelname)s - %(message)s'
21
  )
22
+ logger = logging.getLogger("ArchiverPro")
23
 
24
+ # ============ الإعدادات والمتغيرات ============
25
  API_ID = int(os.environ.get("API_ID", 0))
26
  API_HASH = os.environ.get("API_HASH", "")
27
  SESSION_STRING = os.environ.get("SESSION_STRING", "")
28
  TARGET_CHAT_RAW = os.environ.get("TARGET_CHAT", "me")
29
+ # يمكنك وضع قنوات مفصولة بفواصل مثل: "@channel1, -100123456789, @channel2"
30
+ CHANNELS_TO_SCRAPE = os.environ.get("CHANNELS_TO_SCRAPE", "")
31
 
32
+ DATA_DIR = "/tmp/data" if not os.path.exists("/data") else "/data"
33
  DOWNLOAD_DIR = os.path.join(DATA_DIR, "downloads")
34
+ DB_PATH = os.path.join(DATA_DIR, "queue_v2.db")
35
  os.makedirs(DOWNLOAD_DIR, exist_ok=True)
36
 
37
+ # ============ إنشاء العميل (Hydrogram Client) ============
38
  app = Client(
39
+ "archiver_pro",
40
  api_id=API_ID,
41
  api_hash=API_HASH,
42
  session_string=SESSION_STRING,
43
  in_memory=True,
44
+ workers=4,
45
+ device_model="Desktop Archiver",
46
+ system_version="Linux Archiver 64bit",
47
+ app_version="2.0.0",
48
+ lang_code="ar"
 
49
  )
50
 
51
+ TARGET_CHAT = "me"
52
+ stats = {
53
+ "total_queued": 0,
54
+ "total_transferred": 0,
55
+ "total_failed": 0,
56
+ "status": "Starting..."
57
+ }
58
+
59
+ # ============ إدارة الـ FloodWait والتحكم بالسرعة ============
60
+ class RateLimiter:
61
+ def __init__(self, delay=2.5):
62
+ self.delay = delay
63
+ self.last_call = 0
64
+ self.lock = asyncio.Lock()
65
+ self.flood_until = 0
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  async def wait(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  async with self.lock:
69
+ # التحقق من وجود حظر FloodWait
70
  now = time.time()
71
+ if self.flood_until > now:
72
+ wait_sec = self.flood_until - now
73
+ logger.warning(f"⏳ FloodWait فعال، انتظار {wait_sec:.1f} ثانية...")
74
+ await asyncio.sleep(wait_sec)
75
+
76
+ # التحقق من الفاصل الزمني العادي لتفادي الحظر
77
+ now = time.time()
78
+ elapsed = now - self.last_call
79
+ if elapsed < self.delay:
80
+ await asyncio.sleep(self.delay - elapsed)
81
+ self.last_call = time.time()
82
 
83
+ def set_flood(self, seconds):
84
+ self.flood_until = time.time() + seconds + 2
85
+ logger.warning(f"🚨 تم استلام FloodWait: {seconds}s")
86
 
87
+ rate_limiter = RateLimiter(delay=2.0)
 
 
88
 
89
+ # ============ قاعدة البيانات وحفظ الطابور ============
90
  async def init_db():
91
  async with aiosqlite.connect(DB_PATH) as db:
92
  await db.execute("""
 
96
  message_id INTEGER NOT NULL,
97
  media_group_id TEXT,
98
  media_type TEXT,
 
 
 
 
99
  caption TEXT,
100
  source_name TEXT,
101
+ album_total INTEGER DEFAULT 1,
102
+ album_index INTEGER DEFAULT 1,
103
+ status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
104
  attempts INTEGER DEFAULT 0,
105
  error TEXT,
106
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 
107
  UNIQUE(chat_id, message_id)
108
  )
109
  """)
110
  await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)")
111
  await db.execute("CREATE INDEX IF NOT EXISTS idx_jobs_media_group ON jobs(media_group_id)")
112
  await db.commit()
113
+ logger.info("✅ تم تهيئة قاعدة البيانات بنجاح.")
114
 
115
+ async def add_message_to_queue(message, album_info=None):
116
+ if not message.media:
117
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  chat_id = int(message.chat.id)
120
  message_id = int(message.id)
 
 
 
121
  media_group_id = str(message.media_group_id) if message.media_group_id else None
122
 
123
+ # تحديد نوع الميديا
124
+ media_type = "unknown"
125
+ if message.photo: media_type = "photo"
126
+ elif message.video: media_type = "video"
127
+ elif message.document: media_type = "document"
128
+ elif message.audio: media_type = "audio"
129
+ elif message.voice: media_type = "voice"
130
+ elif message.video_note: media_type = "video_note"
131
+ elif message.animation: media_type = "animation"
132
+
133
+ # اسم القناة أو المجموعة
134
+ chat = message.chat
135
+ source_name = chat.title or chat.first_name or chat.username or f"Chat_{chat_id}"
136
+
137
+ raw_caption = message.caption or ""
138
 
139
+ # تفاصيل الألبوم إن وجدت
140
+ album_total = 1
141
+ album_index = 1
142
+ if album_info:
143
+ album_total = album_info.get("total", 1)
144
+ album_index = album_info.get("index", 1)
145
+
146
  async with aiosqlite.connect(DB_PATH) as db:
147
  try:
148
  await db.execute("""
149
  INSERT OR IGNORE INTO jobs
150
+ (chat_id, message_id, media_group_id, media_type, caption, source_name, album_total, album_index, status)
151
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending')
152
+ """, (chat_id, message_id, media_group_id, media_type, raw_caption, source_name, album_total, album_index))
 
 
 
153
  await db.commit()
154
+ stats["total_queued"] += 1
155
+ return True
156
  except Exception as e:
157
+ logger.error(f"خطأ في إضافة الرسالة لقاعدة البيانات: {e}")
158
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # ============ إعداد الكابشن الذكي ============
161
+ def format_caption(raw_cap, source_name, album_index, album_total):
162
+ lines = []
163
+ if raw_cap and raw_cap.strip():
164
+ lines.append(raw_cap.strip())
165
+ lines.append("")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ lines.append(f"📢 **المصدر:** {source_name}")
168
+ if album_total > 1:
169
+ lines.append(f"🔢 **الجزء:** {album_index} من {album_total}")
 
 
170
 
171
+ return "\n".join(lines)
172
+
173
+ # ============ النقل الذكي (Fast Copy مع Fallback للتحميل) ============
174
+ async def process_single_job(job):
175
+ chat_id = job["chat_id"]
176
+ msg_id = job["message_id"]
177
+ raw_caption = job["caption"]
178
+ source_name = job["source_name"]
179
+ album_index = job["album_index"]
180
+ album_total = job["album_total"]
181
 
182
+ caption = format_caption(raw_caption, source_name, album_index, album_total)
183
+
184
+ await rate_limiter.wait()
185
 
186
+ # 1. المحاولة الأولى: النسخ المباشر فائق السرعة بدون تحميل
187
  try:
188
+ await app.copy_message(
189
+ chat_id=TARGET_CHAT,
190
+ from_chat_id=chat_id,
191
+ message_id=msg_id,
192
+ caption=caption
193
+ )
194
+ logger.info(f"⚡ تم النقل السريع بنجاح للرسالة {msg_id} من {source_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  return True
196
+ except (FloodWait, FloodPremiumWait) as e:
197
+ rate_limiter.set_flood(e.value)
198
+ raise e
199
+ except ChatForwardsRestricted:
200
+ logger.warning(f"⚠️ القناة محمية المحتوى! التحويل إلى وضع التحميل والرفع للرسالة {msg_id}...")
201
+ except MessageIdInvalid:
202
+ logger.error(f"❌ الرسالة {msg_id} محذوفة أو غير صالحة.")
203
+ return "DELETED"
204
  except Exception as e:
205
+ logger.warning(f"⚠️ فشل النسخ المباشر للرسالة {msg_id} ({e})، جاري محاولة التحميل والرفع...")
 
 
 
206
 
207
+ # 2. المحاولة الثانية: التحميل والرفع (في حال كانت القناة تمنع النقل المباشر)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  try:
209
+ msg = await app.get_messages(chat_id, msg_id)
210
+ if not msg or not msg.media:
211
+ return "DELETED"
212
+
213
+ file_path = os.path.join(DOWNLOAD_DIR, f"temp_{chat_id}_{msg_id}")
214
+ downloaded = await app.download_media(msg, file_name=file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
+ if not downloaded or not os.path.exists(downloaded):
217
+ raise Exception("فشل تنزيل الملف")
218
+
219
+ # إعادة الإرسال حسب النوع
220
+ mtype = job["media_type"]
221
+ if mtype == "photo":
222
+ await app.send_photo(TARGET_CHAT, photo=downloaded, caption=caption)
223
+ elif mtype == "video":
224
+ await app.send_video(TARGET_CHAT, video=downloaded, caption=caption, supports_streaming=True)
225
+ elif mtype == "document":
226
+ await app.send_document(TARGET_CHAT, document=downloaded, caption=caption)
227
+ elif mtype == "audio":
228
+ await app.send_audio(TARGET_CHAT, audio=downloaded, caption=caption)
229
+ elif mtype == "voice":
230
+ await app.send_voice(TARGET_CHAT, voice=downloaded, caption=caption)
231
+ elif mtype == "video_note":
232
+ await app.send_video_note(TARGET_CHAT, video_note=downloaded)
233
+ elif mtype == "animation":
234
+ await app.send_animation(TARGET_CHAT, animation=downloaded, caption=caption)
235
+ else:
236
+ await app.send_document(TARGET_CHAT, document=downloaded, caption=caption)
237
+
238
+ # تنظيف الملف المؤقت فوراً لتوفير المساحة
239
+ if os.path.exists(downloaded):
240
+ os.remove(downloaded)
241
+
242
+ logger.info(f"📥 تم التحميل والرفع بنجاح للرسالة {msg_id}")
243
  return True
244
+ except (FloodWait, FloodPremiumWait) as e:
245
+ rate_limiter.set_flood(e.value)
246
+ raise e
247
  except Exception as e:
248
+ logger.error(f"❌ فشل تام في معالجة الرسالة {msg_id}: {e}")
 
249
  return False
250
 
251
+ # ============ عامل معالجة الطابور (Worker) ============
252
+ async def queue_worker():
253
+ logger.info("🚀 بدء تشغيل عامل الطابور...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  while True:
255
  try:
256
  if not app.is_connected:
257
+ await asyncio.sleep(5)
 
258
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  async with aiosqlite.connect(DB_PATH) as db:
261
  db.row_factory = aiosqlite.Row
 
262
  cursor = await db.execute("""
263
  SELECT * FROM jobs
264
+ WHERE status = 'pending' AND attempts < 5
265
+ ORDER BY id ASC LIMIT 1
266
  """)
267
+ job_row = await cursor.fetchone()
268
 
269
+ if not job_row:
270
+ await asyncio.sleep(2)
271
+ continue
272
+
273
+ job = dict(job_row)
274
+ job_id = job["id"]
275
+
276
+ await db.execute("UPDATE jobs SET status='processing' WHERE id=?", (job_id,))
277
+ await db.commit()
278
+
279
+ # المعالجة
280
+ result = await process_single_job(job)
 
 
 
 
 
 
 
 
 
 
281
 
282
+ async with aiosqlite.connect(DB_PATH) as db:
283
+ if result is True:
284
+ await db.execute("UPDATE jobs SET status='completed' WHERE id=?", (job_id,))
285
+ stats["total_transferred"] += 1
286
+ elif result == "DELETED":
287
+ await db.execute("UPDATE jobs SET status='failed', error='Message Deleted/Invalid' WHERE id=?", (job_id,))
288
+ stats["total_failed"] += 1
289
+ else:
290
+ await db.execute("""
291
+ UPDATE jobs SET status='pending', attempts=attempts+1, error='Retry later' WHERE id=?
292
+ """, (job_id,))
293
+ await db.commit()
294
+
295
+ except (FloodWait, FloodPremiumWait) as e:
296
+ logger.warning(f"انتظار FloodWait: {e.value}s")
297
+ await asyncio.sleep(e.value + 1)
298
  except Exception as e:
299
+ logger.error(f"خطأ غير متوقع في الـ Worker: {e}")
300
+ await asyncio.sleep(5)
301
 
302
+ # ============ ماسح الرسائل القديمة (History Scraper) ============
303
+ async def scrape_channel_history(chat_identifier, limit=0):
304
+ """
305
+ يقوم بجلب كافة الرسائل القديمة من قناة أو مجموعة وترتيبها في الطابور
306
+ """
307
  try:
308
+ chat = await app.get_chat(chat_identifier)
309
+ chat_id = chat.id
310
+ chat_title = chat.title or chat.first_name or chat_identifier
311
+ logger.info(f"🔄 بدء جلب الرسائل القديمة من: {chat_title} ({chat_id})")
312
+
313
+ count = 0
314
+ media_groups = {}
315
+
316
+ # قراءة الأرشيف من الأقدم للأحدث أو من الأحدث للأقدم
317
+ async for message in app.get_chat_history(chat_id, limit=limit if limit > 0 else 0):
318
+ if message.media:
319
+ # إذا كان ألبوم
320
+ if message.media_group_id:
321
+ mg_id = str(message.media_group_id)
322
+ media_groups.setdefault(mg_id, []).append(message)
323
+ else:
324
+ await add_message_to_queue(message)
325
+ count += 1
326
+
327
+ if count % 50 == 0 and count > 0:
328
+ logger.info(f"📦 تم جدولة {count} رسالة من {chat_title}...")
329
+ await asyncio.sleep(0.1)
330
+
331
+ # إضافة الألبومات مع حساب عدد المقاطع لكل ألبوم
332
+ for mg_id, msgs in media_groups.items():
333
+ # ترتيب رسائل الألبوم بالترتيب الزمني الصحيح
334
+ msgs.sort(key=lambda m: m.id)
335
+ total = len(msgs)
336
+ for idx, msg in enumerate(msgs, start=1):
337
+ await add_message_to_queue(msg, album_info={"total": total, "index": idx})
338
+ count += 1
339
+
340
+ logger.info(f"✅ تم الانتهاء من فحص {chat_title}! إجمالي الوسائط المجدولة: {count}")
341
+ return f"تم فحص {chat_title} بنجاح وجدولة {count} ملف."
342
  except Exception as e:
343
+ logger.error(f"❌ فشل فحص القناة {chat_identifier}: {e}")
344
+ return f"خطأ في فحص {chat_identifier}: {str(e)}"
345
 
346
+ # ============ رصد الرسائل الجديدة لحظياً (Real-time Listener) ============
347
  @app.on_message(filters.incoming & ~filters.me & filters.media)
348
+ async def realtime_media_handler(client, message):
349
+ try:
350
+ # تجنب رصد الرسائل التي ترسلها الوجهة
351
+ if str(message.chat.id) == str(TARGET_CHAT):
352
+ return
353
+
354
+ if message.media_group_id:
355
+ # إذا كان ألبوم، انتظر ثانية واحدة لتكتمل حزمة الألبوم ثم جدولها
356
+ await asyncio.sleep(1.5)
357
+ try:
358
+ group_messages = await client.get_media_group(message.chat.id, message.id)
359
+ group_messages.sort(key=lambda m: m.id)
360
+ total = len(group_messages)
361
+ for idx, msg in enumerate(group_messages, start=1):
362
+ await add_message_to_queue(msg, album_info={"total": total, "index": idx})
363
+ except Exception:
364
+ # إذا فشل جلب المجموعة كاملة، أضف الرسالة مفردة
365
+ await add_message_to_queue(message)
366
+ else:
367
+ await add_message_to_queue(message)
368
+
369
+ logger.info(f"✨ رصد رسالة جديدة من {message.chat.title or message.chat.id}")
370
+ except Exception as e:
371
+ logger.error(f"خطأ في الرصد اللحظي: {e}")
372
 
373
+ # ============ تهيئة الهدف (TARGET_CHAT) ============
374
+ async def resolve_target():
375
  global TARGET_CHAT
376
+ raw = TARGET_CHAT_RAW.strip()
377
+ if raw.lower() in ["me", "saved", ""]:
378
  TARGET_CHAT = "me"
379
+ logger.info("🎯 الوجهة المحددة: الرسائل المحفوظة (Saved Messages)")
380
  return
381
  try:
382
  chat_id_int = int(raw)
383
  chat = await app.get_chat(chat_id_int)
384
  TARGET_CHAT = chat_id_int
385
+ logger.info(f"🎯 الوجهة المحددة: {chat.title} ({TARGET_CHAT})")
386
+ except ValueError:
387
+ try:
388
+ chat = await app.get_chat(raw)
389
+ TARGET_CHAT = chat.id
390
+ logger.info(f"🎯 الوجهة المحددة: {chat.title} ({TARGET_CHAT})")
391
+ except Exception as e:
392
+ logger.error(f"فشل التحقق من الوجهة ({raw}): {e}. تم التحويل للرسائل المحفوظة.")
393
+ TARGET_CHAT = "me"
394
 
395
+ # ============ واجهة التحكم السريعة (Gradio UI) ============
396
+ def create_ui():
397
  with gr.Blocks(title="Telegram Archiver Pro") as demo:
398
+ gr.Markdown("## 🚀 محول وأرشيف تيليجرام الاحترافي (بدون ضياع ملفات)")
399
+
400
+ with gr.Row():
401
+ status_box = gr.Textbox(label="حالة النظام", value="جاري التشغيل...", interactive=False)
402
+ queued_box = gr.Number(label="الملفات في الطابور", value=0, interactive=False)
403
+ transferred_box = gr.Number(label="الملفات المنقولة بنجاح", value=0, interactive=False)
404
+
405
+ gr.Markdown("---")
406
+ gr.Markdown("### 📥 سحب الأرشيف القديم من قناة أو كروب:")
407
+ with gr.Row():
408
+ channel_input = gr.Textbox(
409
+ label="معرف أو رابط القناة / الكروب",
410
+ placeholder="@my_channel أو -100123456789 أو رابط القناة"
411
+ )
412
+ limit_input = gr.Number(label="أقصى عدد رسائل (0 = الكل)", value=0)
413
+ scrape_btn = gr.Button("بدء السحب الآن 📥", variant="primary")
414
+
415
+ scrape_output = gr.Textbox(label="نتيجة الفحص", interactive=False)
416
+
417
+ def do_scrape(ch, lim):
418
+ if not ch:
419
+ return "يرجى كتابة معرف القناة أولاً"
420
+ loop = asyncio.new_event_loop()
421
+ res = loop.run_until_complete(scrape_channel_history(ch.strip(), int(lim)))
422
+ loop.close()
423
+ return res
424
+
425
+ scrape_btn.click(fn=do_scrape, inputs=[channel_input, limit_input], outputs=[scrape_output])
426
+
427
+ return demo
428
+
429
+ # ============ تشغيل خادم الويب و Hydrogram ============
430
+ def run_web():
431
  fastapi_app = FastAPI()
432
+ demo = create_ui()
433
+ gr.mount_gradio_app(fastapi_app, demo, path="/")
 
 
434
 
435
  @fastapi_app.get("/ping")
436
  async def ping():
437
+ return {"status": "alive", "stats": stats}
438
+
 
 
 
439
  uvicorn.run(fastapi_app, host="0.0.0.0", port=7860)
440
 
 
441
  async def main():
442
+ logger.info("جاري تسجيل الدخول إلى تيليجرام...")
 
 
 
 
 
 
443
  await app.start()
444
 
 
 
 
 
 
 
 
 
 
 
445
  me = await app.get_me()
446
+ logger.info(f"✅ تم الدخول بحساب: {me.first_name} (@{me.username or me.id})")
447
+
448
+ await resolve_target()
449
  await init_db()
450
 
451
+ # تشغيل العامل في الخلفية
452
+ asyncio.create_task(queue_worker())
 
 
453
 
454
+ # فحص القنوات المحددة في المتغيرات البيئية إن وجدت
455
+ if CHANNELS_TO_SCRAPE:
456
+ channels = [c.strip() for c in CHANNELS_TO_SCRAPE.split(",") if c.strip()]
457
+ for ch in channels:
458
+ asyncio.create_task(scrape_channel_history(ch))
459
+
460
+ stats["status"] = "يعمل بكفاءة ✅"
461
 
462
+ # الإبقاء قيد التشغيل
463
  stop_event = asyncio.Event()
464
  await stop_event.wait()
465
 
466
  if __name__ == "__main__":
467
+ # تشغيل واجهة الويب في Thread مستقل لمنع تعليق البوت وتلبية متطلبات Hugging Face
468
+ web_thread = threading.Thread(target=run_web, daemon=True)
469
+ web_thread.start()
470
 
471
  loop = asyncio.get_event_loop()
472
  try:
 
474
  except KeyboardInterrupt:
475
  pass
476
  finally:
477
+ if app.is_connected:
478
+ loop.run_until_complete(app.stop())