no-name-here commited on
Commit
8ca9f2e
Β·
verified Β·
1 Parent(s): 04b95f5

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +153 -62
main.py CHANGED
@@ -1,15 +1,9 @@
1
  # Link Generator Bot
2
  # Webhook-only | HuggingFace Spaces compatible
3
- #
4
- # Flow:
5
- # User sends any file/photo/video/audio to the bot
6
- # β†’ file_id extracted from raw webhook update
7
- # β†’ Downloaded via Pyrogram MTProto (bot.download_media)
8
- # β†’ Saved to HF Space local storage
9
- # β†’ Public link returned to user
10
 
11
  import os
12
  import sys
 
13
  import logging
14
  import asyncio
15
  import traceback
@@ -22,7 +16,6 @@ from fastapi.responses import FileResponse, JSONResponse
22
  from pyrogram import Client
23
  from pyrogram.enums import ParseMode
24
  from pyrogram.errors import FloodWait
25
- from pyrogram.types import Message
26
 
27
  from fileManager import FileManager
28
 
@@ -111,6 +104,51 @@ def track_task(coro):
111
  task.add_done_callback(RUNNING_TASKS.discard)
112
  return task
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # ============================================================
115
  # Webhook response helpers
116
  # ============================================================
@@ -153,110 +191,167 @@ def handle_help(chat_id: int) -> dict:
153
 
154
  def handle_stats(chat_id: int) -> dict:
155
  import shutil, psutil
 
156
  total, used, free = shutil.disk_usage(".")
157
  proc = psutil.Process(os.getpid())
158
- def fmt(b):
159
- for u in ["B", "KB", "MB", "GB"]:
160
- if b < 1024: return f"{b:.1f} {u}"
161
- b /= 1024
162
- return f"{b:.1f} TB"
163
- fl_count = len(os.listdir("fl")) if os.path.isdir("fl") else 0
164
  return _msg(chat_id,
165
- "**πŸ“Š Bot Stats**\n\n"
166
- f"**➜ Disk:** `{fmt(used)}` used / `{fmt(total)}` total\n"
167
- f"**➜ Free:** `{fmt(free)}`\n"
168
- f"**➜ Memory:** `{round(proc.memory_info()[0]/1024**2)} MiB`\n"
169
- f"**➜ CPU:** `{psutil.cpu_percent(interval=0.2)}%`\n"
170
- f"**➜ Stored files:** `{fl_count}`")
 
 
171
 
172
  # ============================================================
173
- # Extract file_id + filename from raw webhook message dict
174
  # ============================================================
175
  MEDIA_TYPES = [
176
  "document", "video", "audio", "voice",
177
  "photo", "animation", "sticker", "video_note"
178
  ]
179
-
180
  EXT_MAP = {
181
  "video": "mp4", "audio": "mp3", "voice": "ogg",
182
  "animation": "gif", "sticker": "webp", "video_note": "mp4"
183
  }
184
 
185
  def get_file_info(msg: dict) -> tuple:
186
- """Returns (file_id, filename) or (None, None)."""
187
  for mtype in MEDIA_TYPES:
188
  if mtype not in msg:
189
  continue
190
  if mtype == "photo":
191
- # list of sizes β€” pick the largest
192
  photos = msg["photo"]
193
  obj = photos[-1] if isinstance(photos, list) else photos
194
- return obj["file_id"], f"{obj.get('file_unique_id', 'photo')}.jpg"
195
-
196
  obj = msg[mtype]
197
  fid = obj["file_id"]
198
  fname = obj.get("file_name") or obj.get("file_unique_id", mtype)
199
  if "." not in fname.split("/")[-1]:
200
  fname = f"{fname}.{EXT_MAP.get(mtype, 'bin')}"
201
- return fid, fname
202
-
203
- return None, None
204
 
205
  # ============================================================
206
- # Async media handler β€” uses file_id directly
207
  # ============================================================
208
- async def handle_media(chat_id: int, reply_to_id: int, file_id: str, filename: str):
209
- progress_msg = await bot.send_message(
210
- chat_id, "⏳ **Processing your file...**",
211
- reply_to_message_id=reply_to_id)
212
- try:
213
- tmp_path = f"fl/tmp_{reply_to_id}_{filename}"
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- # Download via Pyrogram MTProto using file_id directly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  for attempt in range(2):
217
  try:
218
- downloaded = await bot.download_media(file_id, file_name=tmp_path)
 
 
 
 
219
  break
220
  except FloodWait as e:
221
  wait_s = int(getattr(e, "value", 0) or 0)
222
  LOGGER(__name__).warning(f"FloodWait: {wait_s}s")
223
  if attempt == 0 and wait_s > 0:
 
 
 
 
224
  await asyncio.sleep(wait_s + 1)
 
225
  continue
226
  raise
227
 
228
- if not downloaded or not os.path.exists(downloaded) or os.path.getsize(downloaded) == 0:
229
- await progress_msg.edit("❌ **Download failed. Please try again.**")
 
 
 
 
 
230
  try: os.remove(tmp_path)
231
  except: pass
232
  return
233
 
234
- # Read bytes β†’ save via FileManager β†’ get public URL
 
 
 
 
 
 
 
 
 
 
 
 
235
  with open(downloaded, "rb") as f:
236
  file_data = f.read()
237
  os.remove(downloaded)
238
 
239
- entry = fm.save_file(file_data, filename)
240
- # Build direct download link served by our own FastAPI endpoint
241
- link = f"{SPACE_URL}/download/{entry['id']}"
242
- file_size = len(file_data)
243
- size_str = (f"{file_size/1024/1024:.2f} MB"
244
- if file_size > 1024 * 1024
245
- else f"{file_size/1024:.1f} KB")
246
 
247
- await progress_msg.edit(
248
- f"βœ… **Link Generated!**\n\n"
 
 
 
249
  f"πŸ”— **Link:**\n`{link}`\n\n"
250
  f"πŸ“„ **File:** `{entry['filename']}`\n"
251
- f"πŸ“¦ **Size:** `{size_str}`\n\n"
252
- f"_Link is public and accessible by anyone._"
 
 
 
 
253
  )
254
- LOGGER(__name__).info(f"Generated link for {filename}: {link}")
255
 
256
  except Exception as e:
257
  LOGGER(__name__).error(traceback.format_exc())
258
  try:
259
- await progress_msg.edit(f"❌ **Error:** `{str(e)}`")
 
 
 
 
260
  except Exception:
261
  pass
262
 
@@ -267,7 +362,8 @@ async def handle_media(chat_id: int, reply_to_id: int, file_id: str, filename: s
267
  async def lifespan(app: FastAPI):
268
  LOGGER(__name__).info("Starting bot client…")
269
  await bot.start()
270
- LOGGER(__name__).info("Bot ready β€” waiting for webhook updates.")
 
271
  yield
272
  LOGGER(__name__).info("Shutting down…")
273
  await bot.stop()
@@ -281,10 +377,8 @@ app = FastAPI(lifespan=lifespan)
281
  async def root():
282
  return {"status": "ok", "service": "Link Generator Bot"}
283
 
284
-
285
  @app.get("/download/{file_id}")
286
  async def download_file(file_id: str):
287
- """Serve a stored file by its UUID."""
288
  entry = fm.get_entry_by_id(file_id)
289
  if not entry:
290
  return JSONResponse({"error": "File not found or expired"}, status_code=404)
@@ -313,7 +407,6 @@ async def telegram_webhook(request: Request):
313
  text = msg.get("text", "").strip()
314
  msg_id = msg["message_id"]
315
 
316
- # ── commands ──────────────────────────────────────────
317
  if text.startswith("/start"):
318
  return JSONResponse(handle_start(chat_id))
319
  if text.startswith("/help"):
@@ -321,13 +414,11 @@ async def telegram_webhook(request: Request):
321
  if text.startswith("/stats"):
322
  return JSONResponse(handle_stats(chat_id))
323
 
324
- # ── media ─────────────────────────────────────────────
325
- file_id, filename = get_file_info(msg)
326
  if file_id:
327
- track_task(handle_media(chat_id, msg_id, file_id, filename))
328
  return JSONResponse({"status": "ok"})
329
 
330
- # ── unknown text ──────────────────────────────────────
331
  if text and not text.startswith("/"):
332
  return JSONResponse(_msg(chat_id,
333
  "πŸ“Ž **Please send a file, photo, video or audio.**\n"
 
1
  # Link Generator Bot
2
  # Webhook-only | HuggingFace Spaces compatible
 
 
 
 
 
 
 
3
 
4
  import os
5
  import sys
6
+ import time
7
  import logging
8
  import asyncio
9
  import traceback
 
16
  from pyrogram import Client
17
  from pyrogram.enums import ParseMode
18
  from pyrogram.errors import FloodWait
 
19
 
20
  from fileManager import FileManager
21
 
 
104
  task.add_done_callback(RUNNING_TASKS.discard)
105
  return task
106
 
107
+ # ============================================================
108
+ # UX helpers
109
+ # ============================================================
110
+ def fmt_size(b: float) -> str:
111
+ for u in ["B", "KB", "MB", "GB"]:
112
+ if b < 1024:
113
+ return f"{b:.1f} {u}"
114
+ b /= 1024
115
+ return f"{b:.1f} TB"
116
+
117
+ def fmt_time(seconds: float) -> str:
118
+ seconds = max(0, int(seconds))
119
+ if seconds < 60:
120
+ return f"{seconds}s"
121
+ m, s = divmod(seconds, 60)
122
+ if m < 60:
123
+ return f"{m}m {s}s"
124
+ h, m = divmod(m, 60)
125
+ return f"{h}h {m}m"
126
+
127
+ def progress_bar(current: int, total: int, length: int = 10) -> str:
128
+ if total <= 0:
129
+ return "β–“" * length
130
+ filled = int(length * current / total)
131
+ empty = length - filled
132
+ return "β–“" * filled + "β–‘" * empty
133
+
134
+ def build_progress_text(phase: str, current: int, total: int,
135
+ speed: float, elapsed: float, filename: str) -> str:
136
+ pct = min(100, int(current * 100 / total)) if total > 0 else 0
137
+ bar = progress_bar(current, total)
138
+ eta = fmt_time((total - current) / speed) if speed > 0 else "β€”"
139
+ icons = {"download": "πŸ“₯", "upload": "πŸ“€"}
140
+ icon = icons.get(phase, "βš™οΈ")
141
+ label = "Downloading" if phase == "download" else "Saving"
142
+
143
+ return (
144
+ f"{icon} **{label}:** `{filename}`\n\n"
145
+ f"`{bar}` **{pct}%**\n\n"
146
+ f"**Done:** `{fmt_size(current)}` / `{fmt_size(total)}`\n"
147
+ f"**Speed:** `{fmt_size(speed)}/s`\n"
148
+ f"**ETA:** `{eta}`\n"
149
+ f"**Elapsed:** `{fmt_time(elapsed)}`"
150
+ )
151
+
152
  # ============================================================
153
  # Webhook response helpers
154
  # ============================================================
 
191
 
192
  def handle_stats(chat_id: int) -> dict:
193
  import shutil, psutil
194
+ from time import time as _time
195
  total, used, free = shutil.disk_usage(".")
196
  proc = psutil.Process(os.getpid())
197
+ fl_count = len([f for f in os.listdir("fl")
198
+ if not f.startswith("tmp_")]) if os.path.isdir("fl") else 0
199
+ active = len([t for t in RUNNING_TASKS if not t.done()])
 
 
 
200
  return _msg(chat_id,
201
+ "πŸ“Š **Bot Statistics**\n"
202
+ "━━━━━━━━━━━━━━━━━━━\n"
203
+ f"πŸ’Ύ **Disk Used:** `{fmt_size(used)}` / `{fmt_size(total)}`\n"
204
+ f"πŸ†“ **Disk Free:** `{fmt_size(free)}`\n"
205
+ f"🧠 **Memory:** `{round(proc.memory_info()[0]/1024**2)} MiB`\n"
206
+ f"⚑ **CPU:** `{psutil.cpu_percent(interval=0.2)}%`\n"
207
+ f"πŸ“ **Stored Files:** `{fl_count}`\n"
208
+ f"βš™οΈ **Active Jobs:** `{active}`")
209
 
210
  # ============================================================
211
+ # Extract file_id + filename + size from raw webhook message
212
  # ============================================================
213
  MEDIA_TYPES = [
214
  "document", "video", "audio", "voice",
215
  "photo", "animation", "sticker", "video_note"
216
  ]
 
217
  EXT_MAP = {
218
  "video": "mp4", "audio": "mp3", "voice": "ogg",
219
  "animation": "gif", "sticker": "webp", "video_note": "mp4"
220
  }
221
 
222
  def get_file_info(msg: dict) -> tuple:
223
+ """Returns (file_id, filename, file_size) or (None, None, 0)."""
224
  for mtype in MEDIA_TYPES:
225
  if mtype not in msg:
226
  continue
227
  if mtype == "photo":
 
228
  photos = msg["photo"]
229
  obj = photos[-1] if isinstance(photos, list) else photos
230
+ return obj["file_id"], f"{obj.get('file_unique_id', 'photo')}.jpg", obj.get("file_size", 0)
 
231
  obj = msg[mtype]
232
  fid = obj["file_id"]
233
  fname = obj.get("file_name") or obj.get("file_unique_id", mtype)
234
  if "." not in fname.split("/")[-1]:
235
  fname = f"{fname}.{EXT_MAP.get(mtype, 'bin')}"
236
+ return fid, fname, obj.get("file_size", 0)
237
+ return None, None, 0
 
238
 
239
  # ============================================================
240
+ # Async media handler with live progress
241
  # ============================================================
242
+ async def handle_media(chat_id: int, reply_to_id: int,
243
+ file_id: str, filename: str, file_size: int):
244
+
245
+ short_name = filename if len(filename) <= 30 else filename[:27] + "..."
246
+
247
+ # ── Phase 0: initial status ──────────────────────────────
248
+ status_msg = await bot.send_message(
249
+ chat_id,
250
+ f"πŸ” **Preparing to process your file…**\n\n"
251
+ f"πŸ“„ `{short_name}`\n"
252
+ f"πŸ“¦ Size: `{fmt_size(file_size) if file_size else 'unknown'}`",
253
+ reply_to_message_id=reply_to_id,
254
+ )
255
+
256
+ dl_start = time.time()
257
+ last_edit = [0.0] # mutable for closure
258
+ tmp_path = f"fl/tmp_{reply_to_id}_{filename}"
259
 
260
+ # ── Progress callback for Pyrogram download ──────────────
261
+ async def on_progress(current: int, total: int):
262
+ now = time.time()
263
+ if now - last_edit[0] < 2.5: # throttle edits to every 2.5s
264
+ return
265
+ last_edit[0] = now
266
+ elapsed = now - dl_start
267
+ speed = current / elapsed if elapsed > 0 else 0
268
+ try:
269
+ await status_msg.edit(
270
+ build_progress_text("download", current, total,
271
+ speed, elapsed, short_name)
272
+ )
273
+ except Exception:
274
+ pass
275
+
276
+ try:
277
+ # ── Phase 1: Download ────────────────────────────────
278
+ downloaded = None
279
  for attempt in range(2):
280
  try:
281
+ downloaded = await bot.download_media(
282
+ file_id,
283
+ file_name=tmp_path,
284
+ progress=on_progress,
285
+ )
286
  break
287
  except FloodWait as e:
288
  wait_s = int(getattr(e, "value", 0) or 0)
289
  LOGGER(__name__).warning(f"FloodWait: {wait_s}s")
290
  if attempt == 0 and wait_s > 0:
291
+ await status_msg.edit(
292
+ f"⏳ **FloodWait β€” retrying in {wait_s}s…**\n\n"
293
+ f"πŸ“„ `{short_name}`"
294
+ )
295
  await asyncio.sleep(wait_s + 1)
296
+ dl_start = time.time()
297
  continue
298
  raise
299
 
300
+ if not downloaded or not os.path.exists(downloaded) \
301
+ or os.path.getsize(downloaded) == 0:
302
+ await status_msg.edit(
303
+ "❌ **Download Failed**\n\n"
304
+ "Could not retrieve the file from Telegram.\n"
305
+ "Please try again."
306
+ )
307
  try: os.remove(tmp_path)
308
  except: pass
309
  return
310
 
311
+ actual_size = os.path.getsize(downloaded)
312
+ dl_elapsed = time.time() - dl_start
313
+ dl_speed = actual_size / dl_elapsed if dl_elapsed > 0 else 0
314
+
315
+ # ── Phase 2: Saving ──────────────────────────────────
316
+ await status_msg.edit(
317
+ f"πŸ“€ **Saving to server…**\n\n"
318
+ f"πŸ“„ `{short_name}`\n"
319
+ f"πŸ“¦ `{fmt_size(actual_size)}`\n\n"
320
+ f"⬇️ Downloaded in `{fmt_time(dl_elapsed)}` "
321
+ f"at `{fmt_size(dl_speed)}/s`"
322
+ )
323
+
324
  with open(downloaded, "rb") as f:
325
  file_data = f.read()
326
  os.remove(downloaded)
327
 
328
+ entry = fm.save_file(file_data, filename)
329
+ link = f"{SPACE_URL}/download/{entry['id']}"
 
 
 
 
 
330
 
331
+ # ── Phase 3: Done ────────────────────────────────────
332
+ total_elapsed = time.time() - dl_start
333
+ await status_msg.edit(
334
+ f"βœ… **Link Generated Successfully!**\n"
335
+ f"━━━━━━━━━━━━━━━━━━━\n"
336
  f"πŸ”— **Link:**\n`{link}`\n\n"
337
  f"πŸ“„ **File:** `{entry['filename']}`\n"
338
+ f"πŸ“¦ **Size:** `{fmt_size(actual_size)}`\n"
339
+ f"⚑ **Speed:** `{fmt_size(dl_speed)}/s`\n"
340
+ f"⏱ **Time:** `{fmt_time(total_elapsed)}`\n"
341
+ f"━━━━━━━━━━━━━━━━━━━\n"
342
+ f"_Tap the link to copy β€’ Accessible by anyone_",
343
+ disable_web_page_preview=True,
344
  )
345
+ LOGGER(__name__).info(f"Generated link for {filename} ({fmt_size(actual_size)}): {link}")
346
 
347
  except Exception as e:
348
  LOGGER(__name__).error(traceback.format_exc())
349
  try:
350
+ await status_msg.edit(
351
+ f"❌ **Something went wrong**\n\n"
352
+ f"`{str(e)[:200]}`\n\n"
353
+ f"Please try again or contact support."
354
+ )
355
  except Exception:
356
  pass
357
 
 
362
  async def lifespan(app: FastAPI):
363
  LOGGER(__name__).info("Starting bot client…")
364
  await bot.start()
365
+ me = await bot.get_me()
366
+ LOGGER(__name__).info(f"Bot ready: @{me.username} β€” waiting for webhook updates.")
367
  yield
368
  LOGGER(__name__).info("Shutting down…")
369
  await bot.stop()
 
377
  async def root():
378
  return {"status": "ok", "service": "Link Generator Bot"}
379
 
 
380
  @app.get("/download/{file_id}")
381
  async def download_file(file_id: str):
 
382
  entry = fm.get_entry_by_id(file_id)
383
  if not entry:
384
  return JSONResponse({"error": "File not found or expired"}, status_code=404)
 
407
  text = msg.get("text", "").strip()
408
  msg_id = msg["message_id"]
409
 
 
410
  if text.startswith("/start"):
411
  return JSONResponse(handle_start(chat_id))
412
  if text.startswith("/help"):
 
414
  if text.startswith("/stats"):
415
  return JSONResponse(handle_stats(chat_id))
416
 
417
+ file_id, filename, file_size = get_file_info(msg)
 
418
  if file_id:
419
+ track_task(handle_media(chat_id, msg_id, file_id, filename, file_size))
420
  return JSONResponse({"status": "ok"})
421
 
 
422
  if text and not text.startswith("/"):
423
  return JSONResponse(_msg(chat_id,
424
  "πŸ“Ž **Please send a file, photo, video or audio.**\n"