understanding commited on
Commit
00608ed
·
verified ·
1 Parent(s): 5d41ce9

Update bot/handlers.py

Browse files
Files changed (1) hide show
  1. bot/handlers.py +291 -433
bot/handlers.py CHANGED
@@ -1,7 +1,4 @@
1
  # PATH: bot/handlers.py
2
- # NOTE: Fixes callback "not allowed" bug by using q.from_user.id for inline buttons.
3
- # Also includes basic auth flow handlers for AUTH_JSON / AUTH_CI / BACK / CANCEL.
4
-
5
  from __future__ import annotations
6
 
7
  import asyncio
@@ -14,25 +11,13 @@ from hydrogram import Client, filters
14
  from hydrogram.types import CallbackQuery, Message
15
 
16
  from bot.config import Auth, Workers
17
- from bot.core.progress import SpeedETA, human_bytes, human_eta
18
- from bot.core.settings import Settings
19
- from bot.core.uptime import uptime_text
20
  from bot.integrations.auth import allow_user, disallow_user, get_stats, is_allowed
21
- from bot.integrations.cf_worker1 import profile_add, profile_check_auth, profile_delete
22
- from bot.integrations.cf_worker2 import (
23
- get_default_profile,
24
- list_profiles,
25
- record_upload,
26
- set_default_profile,
27
- )
28
  from bot.telegram.files import cleanup_file
29
  from bot.telegram.media import download_to_temp
30
  from bot.telegram.replies import safe_edit, safe_reply
31
  from bot.ui.callbacks import (
32
- AUTH_CI,
33
- AUTH_JSON,
34
- BACK,
35
- CANCEL,
36
  MENU_AUTH,
37
  MENU_HELP,
38
  MENU_PROFILES,
@@ -41,25 +26,31 @@ from bot.ui.callbacks import (
41
  UP_EDIT,
42
  UP_GO,
43
  UP_PRIV,
 
 
 
 
 
44
  )
45
  from bot.ui.keyboards import auth_menu_keyboard, main_menu_keyboard, profiles_keyboard, upload_confirm_keyboard
46
  from bot.ui.parse import parse_cb
47
  from bot.ui.texts import CANCELLED, HELP_TEXT, NEED_AUTH, NOT_ALLOWED, OWNER_ONLY
 
 
 
48
  from bot.youtube.link_parser import parse_telegram_link
49
 
50
 
51
- # ============================================================
52
- # STATE
53
- # ============================================================
54
-
55
  @dataclass
56
  class PendingUpload:
57
  src_msg: Message
58
  downloader: Client
59
  file_name: str
 
60
  title: str
61
  description: str
62
  privacy: str = "private"
 
63
  status_msg: Optional[Message] = None
64
  via_link: bool = False
65
 
@@ -71,38 +62,39 @@ class EditState:
71
  privacy: str
72
 
73
 
74
- # In-memory state
75
- _PENDING_UPLOAD: Dict[int, PendingUpload] = {} # uid -> pending upload
76
- _AWAIT_EDIT: Dict[int, EditState] = {} # uid -> editing title/desc
77
- _AWAIT_AUTH_MODE: Dict[int, str] = {} # uid -> "json" | "ci"
78
- _PENDING_DELETE: Dict[int, str] = {} # uid -> profile_id to delete
79
- _IN_PROGRESS: Dict[int, bool] = {} # uid -> upload is running
80
- _UPLOAD_TASK: Dict[int, asyncio.Task] = {} # uid -> running upload task (so /cancel can stop it)
81
- _BATCH_TASK: Dict[int, asyncio.Task] = {} # uid -> running batch task
82
-
83
 
84
- # ============================================================
85
- # HELPERS
86
- # ============================================================
87
 
88
  def _is_admin_or_owner(uid: int) -> bool:
89
- return uid in set(Auth.OWNERS or []) or uid in set(Auth.ADMINS or [])
90
 
91
 
92
- async def _ensure_allowed(reply_target: Message, uid: int | None = None) -> bool:
 
 
 
 
 
 
 
 
 
 
93
  """
94
- IMPORTANT: For callback queries, pass uid=q.from_user.id
95
- because q.message.from_user is the BOT (not the clicker).
96
  """
97
- if uid is None:
98
- uid = int(getattr(getattr(reply_target, "from_user", None), "id", 0) or 0)
99
-
100
  if _is_admin_or_owner(uid):
101
  return True
102
-
103
  ok = await is_allowed(uid)
104
  if not ok:
105
- await safe_reply(reply_target, NOT_ALLOWED)
106
  return ok
107
 
108
 
@@ -114,128 +106,153 @@ def _media_and_filename(m: Message) -> Tuple[Optional[Any], str, int]:
114
  return None, "", 0
115
 
116
 
117
- def extract_title_description(src: Message, file_name: str) -> Tuple[str, str]:
118
- # Prefer caption text
119
- caption = (src.caption or "").strip()
120
- if caption:
121
- parts = caption.splitlines()
122
- title = (parts[0] or "").strip()
123
- desc = "\n".join([p.strip() for p in parts[1:]]).strip()
124
- return title[: Settings.MAX_TITLE], desc[: Settings.MAX_DESC]
125
-
126
  base = os.path.splitext(file_name)[0].strip()
127
- title = (base or "Untitled")[: Settings.MAX_TITLE]
128
- return title, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
 
131
  def _render_preview(p: PendingUpload, size: int) -> str:
 
 
 
 
132
  return (
133
- "📦 Ready to upload\n\n"
134
- f"File: {p.file_name}\n"
135
- f"Size: {human_bytes(size)}\n"
136
- f"Privacy: {p.privacy}\n\n"
137
- f"Title: {p.title}\n"
138
- f"Description: {(p.description[:600] + '…') if len(p.description) > 600 else (p.description or '—')}"
 
 
 
139
  )
140
 
141
 
142
- async def _start_pending_upload(uid: int, src: Message, downloader: Client, via_link: bool = False) -> None:
 
 
 
 
 
 
 
 
143
  media, file_name, size = _media_and_filename(src)
144
  if not media:
 
 
 
 
145
  return
146
 
147
- title, desc = extract_title_description(src, file_name)
 
 
 
 
148
 
149
  p = PendingUpload(
150
  src_msg=src,
151
  downloader=downloader,
152
  file_name=file_name,
 
153
  title=title,
154
  description=desc,
155
  privacy="private",
 
156
  via_link=via_link,
157
  status_msg=None,
158
  )
159
- _PENDING_UPLOAD[uid] = p
160
 
161
  txt = _render_preview(p, size)
162
- await safe_reply(src, txt, reply_markup=upload_confirm_keyboard(p.privacy))
163
-
164
-
165
- def _as_int(v: Any) -> int:
166
- if v is None:
167
- return 0
168
- if isinstance(v, bool):
169
- return int(v)
170
- if isinstance(v, (int, float)):
171
- return int(v)
172
- if isinstance(v, str):
173
- try:
174
- return int(float(v.strip()))
175
- except Exception:
176
- return 0
177
- if isinstance(v, dict):
178
- for k in ("current", "sent", "bytes", "done", "downloaded", "uploaded", "processed", "total"):
179
- if k in v:
180
- try:
181
- return int(float(v.get(k) or 0))
182
- except Exception:
183
- continue
184
- try:
185
- return int(float(next(iter(v.values()))))
186
- except Exception:
187
- return 0
188
- return 0
189
-
190
 
191
- def _extract_client_secrets_from_json(obj: dict) -> tuple[str, str]:
192
- """
193
- Accepts either:
194
- {"client_id": "...", "client_secret": "..."}
195
- or Google style:
196
- {"installed": {"client_id": "...", "client_secret": "..."}}
197
- {"web": {"client_id": "...", "client_secret": "..."}}
198
- """
199
- if not isinstance(obj, dict):
200
- return "", ""
201
-
202
- if obj.get("client_id") and obj.get("client_secret"):
203
- return str(obj.get("client_id") or "").strip(), str(obj.get("client_secret") or "").strip()
204
-
205
- for k in ("installed", "web"):
206
- sub = obj.get(k)
207
- if isinstance(sub, dict) and sub.get("client_id") and sub.get("client_secret"):
208
- return str(sub.get("client_id") or "").strip(), str(sub.get("client_secret") or "").strip()
209
 
210
- return "", ""
 
 
211
 
212
 
213
- async def _run_upload(uid: int) -> None:
214
  if _IN_PROGRESS.get(uid):
215
- return
 
216
  pending = _PENDING_UPLOAD.get(uid)
217
  if not pending:
218
- return
219
- if not pending.status_msg:
220
- return
221
 
222
- _IN_PROGRESS[uid] = True
223
  st = pending.status_msg
 
 
 
 
224
  file_path: Optional[str] = None
225
 
226
  try:
227
- # Ensure there is a default profile
228
  prof = await get_default_profile(uid)
229
  if not (isinstance(prof, dict) and prof.get("ok") and prof.get("profile_id") and prof.get("access_token")):
230
  await safe_edit(st, NEED_AUTH, reply_markup=main_menu_keyboard())
231
- return
232
 
233
  prof_id = str(prof["profile_id"])
234
  access_token = str(prof["access_token"])
235
 
236
  overall_start = time.time()
237
 
238
- # ---- Download (TG -> temp) with live speed ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  await safe_edit(st, "⬇️ Downloading…")
240
  dl_speed = SpeedETA()
241
  dl_last_ui = 0.0
@@ -255,26 +272,23 @@ async def _run_upload(uid: int) -> None:
255
 
256
  rate = dl_speed.update(cur_i, total_i)
257
  txt = (
258
- "⬇️ Downloading…\n\n"
259
- f"{human_bytes(cur_i)} / {human_bytes(total_i)}\n"
260
- f"Speed: {human_bytes(rate)}/s\n"
261
- f"ETA: {human_eta(dl_speed.eta_seconds)}"
262
  )
263
  await safe_edit(st, txt)
264
 
265
- file_path, _, _ = await download_to_temp(
266
- pending.downloader,
267
- pending.src_msg,
268
- progress_cb=dl_progress,
269
- )
270
  dl_dur = max(0.001, time.time() - dl_start)
 
271
  try:
272
  file_bytes = int(os.path.getsize(file_path))
273
  except Exception:
274
  file_bytes = 0
275
 
276
- # ---- Upload (temp -> YouTube) with live speed ----
277
- from bot.youtube.uploader import upload_video # local import
278
 
279
  ul_speed = SpeedETA()
280
  ul_last_ui = 0.0
@@ -294,10 +308,10 @@ async def _run_upload(uid: int) -> None:
294
 
295
  rate = ul_speed.update(sent_i, total_i)
296
  txt = (
297
- "⬆️ Uploading…\n\n"
298
- f"{human_bytes(sent_i)} / {human_bytes(total_i)}\n"
299
- f"Speed: {human_bytes(rate)}/s\n"
300
- f"ETA: {human_eta(ul_speed.eta_seconds)}"
301
  )
302
  await safe_edit(st, txt)
303
 
@@ -319,38 +333,42 @@ async def _run_upload(uid: int) -> None:
319
  d = str(detail)
320
  msg += f"\n`{d[:280]}`"
321
  if "uploadLimitExceeded" in d or "quotaExceeded" in d:
322
- msg += "\n\nℹ️ This looks like a YouTube daily upload/quota limit. Try another profile or wait and retry later."
323
 
324
  await safe_edit(st, msg, reply_markup=main_menu_keyboard())
325
- return
326
-
327
- url = str(up.get("url") or "")
328
 
 
329
  await record_upload(uid, prof_id)
330
 
331
  ul_dur = max(0.001, time.time() - ul_start)
332
  total_dur = max(0.001, time.time() - overall_start)
333
 
334
- dl_avg = (human_bytes(file_bytes / dl_dur) + "/s") if file_bytes and dl_dur else "—"
335
- ul_avg = (human_bytes(file_bytes / ul_dur) + "/s") if file_bytes and ul_dur else "—"
336
 
337
- done = "✅ Uploaded!"
338
  if url:
339
  done += f"\n\n{url}"
340
  done += (
341
- f"\n\nDownload: {dl_dur:.1f}s • avg {dl_avg}"
342
- f"\nUpload: {ul_dur:.1f}s • avg {ul_avg}"
343
- f"\nTotal: {total_dur:.1f}s"
344
  )
345
  await safe_edit(st, done, reply_markup=main_menu_keyboard())
 
346
 
347
  except asyncio.CancelledError:
348
  try:
349
  await safe_edit(st, CANCELLED, reply_markup=main_menu_keyboard())
350
  except Exception:
351
  pass
 
 
352
  except Exception as e:
353
  await safe_edit(st, f"❌ Error: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
 
 
354
  finally:
355
  _PENDING_UPLOAD.pop(uid, None)
356
  _AWAIT_EDIT.pop(uid, None)
@@ -363,21 +381,15 @@ async def _run_upload(uid: int) -> None:
363
  _UPLOAD_TASK.pop(uid, None)
364
 
365
 
366
- # ============================================================
367
- # HANDLERS
368
- # ============================================================
369
-
370
  def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
371
- # ---- basic ----
372
- @app.on_message(filters.command(["start", "help"]) & filters.private)
373
  async def start_cmd(_, m: Message) -> None:
374
  from bot.ui.texts import START_TEXT
375
  await safe_reply(m, START_TEXT, reply_markup=main_menu_keyboard())
376
 
377
- @app.on_message(filters.command("cancel") & filters.private)
378
  async def cancel_cmd(_, m: Message) -> None:
379
  uid = m.from_user.id
380
-
381
  _AWAIT_AUTH_MODE.pop(uid, None)
382
  _AWAIT_EDIT.pop(uid, None)
383
  _PENDING_UPLOAD.pop(uid, None)
@@ -392,17 +404,14 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
392
  btask.cancel()
393
 
394
  _IN_PROGRESS.pop(uid, None)
395
-
396
  await safe_reply(m, CANCELLED, reply_markup=main_menu_keyboard())
397
 
398
- # ---- admin allowlist ----
399
- @app.on_message(filters.command("allow") & filters.private)
400
  async def allow_cmd(_, m: Message) -> None:
401
  uid = m.from_user.id
402
  if not _is_admin_or_owner(uid):
403
  await safe_reply(m, OWNER_ONLY)
404
  return
405
-
406
  args = (m.text or "").split(maxsplit=1)
407
  if len(args) < 2:
408
  await safe_reply(m, "Usage: /allow <tg_id>")
@@ -412,17 +421,15 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
412
  except Exception:
413
  await safe_reply(m, "Invalid tg_id")
414
  return
415
-
416
  await allow_user(tid)
417
  await safe_reply(m, f"✅ Allowed `{tid}`")
418
 
419
- @app.on_message(filters.command("disallow") & filters.private)
420
  async def disallow_cmd(_, m: Message) -> None:
421
  uid = m.from_user.id
422
  if not _is_admin_or_owner(uid):
423
  await safe_reply(m, OWNER_ONLY)
424
  return
425
-
426
  args = (m.text or "").split(maxsplit=1)
427
  if len(args) < 2:
428
  await safe_reply(m, "Usage: /disallow <tg_id>")
@@ -432,22 +439,19 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
432
  except Exception:
433
  await safe_reply(m, "Invalid tg_id")
434
  return
435
-
436
  await disallow_user(tid)
437
  await safe_reply(m, f"✅ Disallowed `{tid}`")
438
 
439
- @app.on_message(filters.command("stats") & filters.private)
440
  async def stats_cmd(_, m: Message) -> None:
441
  uid = m.from_user.id
442
  if not _is_admin_or_owner(uid):
443
  await safe_reply(m, OWNER_ONLY)
444
  return
445
-
446
  st = await get_stats()
447
  if not isinstance(st, dict):
448
  await safe_reply(m, "❌ stats failed")
449
  return
450
-
451
  await safe_reply(
452
  m,
453
  "📊 *Today stats*\n\n"
@@ -456,7 +460,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
456
  f"Uploads today: `{st.get('uploads_today', 0)}`",
457
  )
458
 
459
- @app.on_message(filters.command("diag") & filters.private)
460
  async def diag_cmd(_, m: Message) -> None:
461
  uid = m.from_user.id
462
  if not _is_admin_or_owner(uid):
@@ -475,16 +479,17 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
475
  f"Owners: `{len(Auth.OWNERS)}` Admins: `{len(Auth.ADMINS)}`",
476
  )
477
 
478
- # ---- auth/profile flow ----
479
- @app.on_message(filters.command("auth") & filters.private)
480
  async def auth_cmd(_, m: Message) -> None:
481
- if not await _ensure_allowed(m):
482
  return
 
 
483
  await safe_reply(m, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
484
 
485
- @app.on_message(filters.command("profiles") & filters.private)
486
  async def profiles_cmd(_, m: Message) -> None:
487
- if not await _ensure_allowed(m):
488
  return
489
  uid = m.from_user.id
490
  data = await list_profiles(uid, only_connected=False)
@@ -495,66 +500,23 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
495
  default_id = data.get("default_profile_id") or ""
496
  await safe_reply(m, "👤 *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
497
 
498
- # ---- upload from DM media ----
499
- @app.on_message(filters.private & (filters.video | filters.document))
500
- async def media_in_dm(_, m: Message) -> None:
501
- # If user is in auth-json mode and sends a JSON document, treat it as auth input.
502
- uid = m.from_user.id
503
- mode = _AWAIT_AUTH_MODE.get(uid)
504
- if mode == "json" and m.document:
505
- fn = str(getattr(m.document, "file_name", "") or "")
506
- if fn.lower().endswith(".json"):
507
- # Download JSON and parse
508
- st = await safe_reply(m, "📄 Reading JSON…")
509
- try:
510
- path, _, _ = await download_to_temp(app, m, progress_cb=None)
511
- if not path or not os.path.exists(path):
512
- await safe_edit(st, "❌ Failed to download JSON file.")
513
- return
514
- try:
515
- import json
516
- with open(path, "r", encoding="utf-8") as f:
517
- obj = json.load(f)
518
- finally:
519
- cleanup_file(path)
520
-
521
- client_id, client_secret = _extract_client_secrets_from_json(obj if isinstance(obj, dict) else {})
522
- label = str((obj.get("label") if isinstance(obj, dict) else "") or "main")[:40]
523
-
524
- if not client_id or not client_secret:
525
- await safe_edit(st, "❌ JSON missing client_id/client_secret. Paste JSON or send ID+Secret.")
526
- return
527
-
528
- out = await profile_add(uid, client_id, client_secret, label=label, ttl_sec=600)
529
- _AWAIT_AUTH_MODE.pop(uid, None)
530
-
531
- if isinstance(out, dict) and out.get("ok") and out.get("login_url"):
532
- await safe_edit(
533
- st,
534
- f"✅ Profile added.\n\n🔗 Login URL:\n{out.get('login_url')}\n\nAfter login, run /profiles.",
535
- reply_markup=main_menu_keyboard(),
536
- )
537
- else:
538
- await safe_edit(st, f"❌ Add failed: `{(out or {}).get('err', 'unknown')}`", reply_markup=main_menu_keyboard())
539
- return
540
- except Exception as e:
541
- await safe_edit(st, f"❌ JSON read failed: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
542
- return
543
-
544
- if not await _ensure_allowed(m):
545
  return
546
-
547
  if _IN_PROGRESS.get(uid):
548
  await safe_reply(m, "⏳ Upload already running. Use /cancel to stop.")
549
  return
550
 
551
- await _start_pending_upload(uid, m, app, via_link=False)
552
 
553
- # ---- upload from link (admin/owner) ----
554
- @app.on_message(filters.command(["yt", "dl", "archive"]) & filters.private)
555
  async def archive_cmd(_, m: Message) -> None:
556
  uid = m.from_user.id
557
- if not await _ensure_allowed(m):
558
  return
559
  if not _is_admin_or_owner(uid):
560
  await safe_reply(m, OWNER_ONLY)
@@ -565,17 +527,17 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
565
 
566
  args = (m.text or "").split(maxsplit=1)
567
  if len(args) < 2:
568
- await safe_reply(m, "Usage: /archive <t.me message link>")
 
 
 
 
569
  return
570
 
571
  try:
572
  chat_ref, msg_id = parse_telegram_link(args[1].strip())
573
  except Exception as e:
574
- await safe_reply(m, f"Bad link: {e}")
575
- return
576
-
577
- st = await safe_reply(m, "🔎 Fetching message…")
578
- if not st:
579
  return
580
 
581
  try:
@@ -584,13 +546,13 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
584
  await safe_edit(st, f"❌ Fetch failed: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
585
  return
586
 
587
- await safe_edit(st, "✅ Message fetched. Preparing preview…", reply_markup=main_menu_keyboard())
588
- await _start_pending_upload(uid, src, user_app, via_link=True)
589
 
590
- # batch mode (owner/admin only): /batch <t.me links...> (one per line)
591
- @app.on_message(filters.command("batch") & filters.private)
592
  async def batch_cmd(_, m: Message) -> None:
593
- if not await _ensure_allowed(m):
594
  return
595
  uid = m.from_user.id
596
  if not _is_admin_or_owner(uid):
@@ -614,151 +576,97 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
614
  await safe_reply(m, "⏳ A batch is already running. Use /cancel to stop it.")
615
  return
616
 
617
- await safe_reply(m, f"🧾 Batch starting: {len(links)} item(s).")
618
 
619
  async def runner() -> None:
620
  batch_start = time.time()
621
  for i, link in enumerate(links, 1):
622
- try:
623
- chat_ref, msg_id = parse_telegram_link(link)
624
- except Exception as e:
625
- await safe_reply(m, f"❌ {i}/{len(links)} bad link: {e}")
626
- continue
627
-
628
  st = await safe_reply(m, f"🔎 Batch {i}/{len(links)}: fetching…")
629
  if not st:
630
  continue
631
 
 
 
 
 
 
 
632
  try:
633
  src = await user_app.get_messages(chat_ref, msg_id)
634
  except Exception as e:
635
  await safe_edit(st, f"❌ Batch {i}/{len(links)} fetch failed: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
636
- continue
637
 
638
- media, file_name, _ = _media_and_filename(src)
639
- if not media:
640
- await safe_edit(st, f"❌ Batch {i}/{len(links)}: no video/document in that message.", reply_markup=main_menu_keyboard())
641
- continue
642
 
643
- title, desc = extract_title_description(src, file_name)
644
-
645
- _PENDING_UPLOAD[uid] = PendingUpload(
646
- src_msg=src,
647
- downloader=user_app,
648
- file_name=file_name,
649
- title=title,
650
- description=desc,
651
- privacy="private",
652
- status_msg=st,
653
- via_link=True,
654
- )
655
 
656
- await safe_edit(st, f"⏳ Batch {i}/{len(links)}: starting upload…", reply_markup=main_menu_keyboard())
657
- await _run_upload(uid)
 
 
658
 
659
  batch_dur = max(0.001, time.time() - batch_start)
660
- await safe_reply(m, f"✅ Batch done in {batch_dur:.1f}s.", reply_markup=main_menu_keyboard())
661
 
662
  t = asyncio.create_task(runner())
663
  _BATCH_TASK[uid] = t
664
 
665
- # ============================================================
666
- # CALLBACKS
667
- # ============================================================
668
- @app.on_callback_query(filters.private)
669
  async def cb_handler(_, q: CallbackQuery) -> None:
670
- # stop Telegram "loading..."
671
- try:
672
- await q.answer()
673
- except Exception:
674
- pass
675
-
676
- uid = int(getattr(getattr(q, "from_user", None), "id", 0) or 0)
677
  action, value = parse_cb(q.data or "")
678
 
679
- # Menus (help is public)
680
  if action == MENU_HELP:
681
  await safe_edit(q.message, HELP_TEXT, reply_markup=main_menu_keyboard())
682
  return
683
 
684
- # Back/cancel
685
  if action == BACK:
686
- _AWAIT_AUTH_MODE.pop(uid, None)
687
- await safe_edit(q.message, "🏠 Menu", reply_markup=main_menu_keyboard())
688
  return
689
 
690
- if action == CANCEL:
691
- _AWAIT_AUTH_MODE.pop(uid, None)
692
- _AWAIT_EDIT.pop(uid, None)
693
- _PENDING_UPLOAD.pop(uid, None)
694
- _PENDING_DELETE.pop(uid, None)
695
- await safe_edit(q.message, CANCELLED, reply_markup=main_menu_keyboard())
696
- return
697
-
698
- # Auth menu actions
699
- if action == MENU_AUTH:
700
- if not await _ensure_allowed(q.message, uid):
701
- return
702
- await safe_edit(q.message, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
703
- return
704
-
705
- if action == AUTH_JSON:
706
- if not await _ensure_allowed(q.message, uid):
707
- return
708
- _AWAIT_AUTH_MODE[uid] = "json"
709
- await safe_edit(
710
- q.message,
711
- "📄 Send your OAuth client JSON now.\n\n"
712
- "• You can paste JSON text here, OR send a .json file.\n"
713
- "• It must contain client_id and client_secret (Google style JSON works).",
714
- reply_markup=main_menu_keyboard(),
715
- )
716
- return
717
-
718
- if action == AUTH_CI:
719
- if not await _ensure_allowed(q.message, uid):
720
- return
721
- _AWAIT_AUTH_MODE[uid] = "ci"
722
- await safe_edit(
723
- q.message,
724
- "🔑 Send Client ID + Secret like this:\n\n"
725
- "client_id_here\n"
726
- "client_secret_here\n"
727
- "[optional label]\n\n"
728
- "Example label: main / alt / channelname",
729
- reply_markup=main_menu_keyboard(),
730
- )
731
- return
732
-
733
- # Speedtest / Profiles
734
  if action == MENU_SPEED:
735
- if not await _ensure_allowed(q.message, uid):
736
  return
737
- from bot.core.speedtest import net_download_test, net_upload_test
 
 
 
 
738
 
739
- await safe_edit(q.message, "⏱ Running speed test…", reply_markup=main_menu_keyboard())
740
  dl = await net_download_test()
741
  ul = await net_upload_test()
742
- try:
743
- dl_bps = float(dl.get("bps", 0) or 0)
744
- except Exception:
745
- dl_bps = 0.0
746
- try:
747
- ul_bps = float(ul.get("bps", 0) or 0)
748
- except Exception:
749
- ul_bps = 0.0
750
 
751
- await safe_edit(
752
- q.message,
753
- "📶 *Speed Test*\n\n"
754
- f"Download: `{human_bytes(dl_bps)}/s`\n"
755
- f"Upload: `{human_bytes(ul_bps)}/s`",
756
- reply_markup=main_menu_keyboard(),
757
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  return
759
 
760
  if action == MENU_PROFILES:
761
- if not await _ensure_allowed(q.message, uid):
762
  return
763
  data = await list_profiles(uid, only_connected=False)
764
  if not (isinstance(data, dict) and data.get("ok")):
@@ -769,11 +677,37 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
769
  await safe_edit(q.message, "👤 *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
770
  return
771
 
772
- # Ignore "noop" buttons
773
- if action == "noop":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
774
  return
775
 
776
- # Upload buttons
777
  if action == UP_PRIV:
778
  p = _PENDING_UPLOAD.get(uid)
779
  if not p:
@@ -782,25 +716,26 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
782
  p.privacy = cycle.get(p.privacy, "private")
783
  _PENDING_UPLOAD[uid] = p
784
  _, _, size = _media_and_filename(p.src_msg)
785
- await safe_edit(q.message, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy))
786
  return
787
 
788
  if action == UP_EDIT:
789
  p = _PENDING_UPLOAD.get(uid)
790
  if not p:
791
  return
 
792
  _AWAIT_EDIT[uid] = EditState(title=p.title, description=p.description, privacy=p.privacy)
 
793
  await safe_edit(
794
  q.message,
795
  "✍️ Send new title + description like this:\n\n"
796
  "Title line\n"
797
  "Description lines…\n\n"
798
  "(Send only 1 line to change title only)",
799
- reply_markup=main_menu_keyboard(),
800
  )
801
  return
802
 
803
- if action == UP_DEL:
804
  _PENDING_UPLOAD.pop(uid, None)
805
  await safe_edit(q.message, CANCELLED, reply_markup=main_menu_keyboard())
806
  return
@@ -814,22 +749,22 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
814
  return
815
  p.status_msg = q.message
816
  _PENDING_UPLOAD[uid] = p
817
- await safe_edit(q.message, "⏳ Starting upload…", reply_markup=main_menu_keyboard())
818
  t = asyncio.create_task(_run_upload(uid))
819
  _UPLOAD_TASK[uid] = t
820
  return
821
 
822
- # Profile callbacks
823
  if action == "pdel":
824
- if not await _ensure_allowed(q.message, uid):
825
  return
826
  pid = value
827
  _PENDING_DELETE[uid] = pid
828
- await safe_edit(q.message, f"❓ Delete profile `{pid}`? Reply YES to confirm.", reply_markup=main_menu_keyboard())
829
  return
830
 
831
  if action == "pdef":
832
- if not await _ensure_allowed(q.message, uid):
833
  return
834
  pid = value
835
  out = await set_default_profile(uid, pid)
@@ -843,7 +778,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
843
  return
844
 
845
  if action == "plog":
846
- if not await _ensure_allowed(q.message, uid):
847
  return
848
  pid = value
849
  chk = await profile_check_auth(uid, pid)
@@ -857,20 +792,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
857
  await safe_edit(q.message, "❌ Failed to create login URL.", reply_markup=main_menu_keyboard())
858
  return
859
 
860
- # Unknown action
861
- return
862
-
863
- # ============================================================
864
- # TEXT INPUT HANDLER (delete confirm + edit + auth input)
865
- # ============================================================
866
- @app.on_message(filters.private & filters.text)
867
- async def text_in_dm(_, m: Message) -> None:
868
- uid = m.from_user.id
869
- txt = (m.text or "").strip()
870
 
871
- # Confirm delete
872
  if uid in _PENDING_DELETE:
873
- if txt.lower() == "yes":
874
  pid = _PENDING_DELETE.pop(uid)
875
  out = await profile_delete(uid, pid)
876
  if isinstance(out, dict) and out.get("ok"):
@@ -882,89 +809,20 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
882
  await safe_reply(m, "Cancelled.", reply_markup=main_menu_keyboard())
883
  return
884
 
885
- # Auth input
886
- if uid in _AWAIT_AUTH_MODE:
887
- if not await _ensure_allowed(m):
888
- return
889
-
890
- mode = _AWAIT_AUTH_MODE.get(uid, "")
891
- if mode == "ci":
892
- lines = [ln.strip() for ln in txt.splitlines() if ln.strip()]
893
- client_id = ""
894
- client_secret = ""
895
- label = "main"
896
-
897
- if len(lines) >= 2:
898
- client_id, client_secret = lines[0], lines[1]
899
- if len(lines) >= 3:
900
- label = lines[2][:40] or "main"
901
- else:
902
- parts = txt.split()
903
- if len(parts) >= 2:
904
- client_id, client_secret = parts[0], parts[1]
905
- if len(parts) > 2:
906
- label = " ".join(parts[2:])[:40] or "main"
907
-
908
- if not client_id or not client_secret:
909
- await safe_reply(m, "❌ Format:\nclient_id\nclient_secret\n[optional label]")
910
- return
911
-
912
- out = await profile_add(uid, client_id, client_secret, label=label, ttl_sec=600)
913
- _AWAIT_AUTH_MODE.pop(uid, None)
914
-
915
- if isinstance(out, dict) and out.get("ok") and out.get("login_url"):
916
- await safe_reply(
917
- m,
918
- f"✅ Profile added.\n\n🔗 Login URL:\n{out.get('login_url')}\n\nAfter login, run /profiles.",
919
- reply_markup=main_menu_keyboard(),
920
- )
921
- else:
922
- await safe_reply(m, f"❌ Add failed: `{(out or {}).get('err', 'unknown')}`", reply_markup=main_menu_keyboard())
923
- return
924
-
925
- if mode == "json":
926
- try:
927
- import json
928
- obj = json.loads(txt)
929
- except Exception:
930
- await safe_reply(m, "❌ Invalid JSON. Paste valid JSON or send a .json file.", reply_markup=main_menu_keyboard())
931
- return
932
-
933
- client_id, client_secret = _extract_client_secrets_from_json(obj if isinstance(obj, dict) else {})
934
- label = str((obj.get("label") if isinstance(obj, dict) else "") or "main")[:40]
935
-
936
- if not client_id or not client_secret:
937
- await safe_reply(m, "❌ JSON missing client_id/client_secret.", reply_markup=main_menu_keyboard())
938
- return
939
-
940
- out = await profile_add(uid, client_id, client_secret, label=label, ttl_sec=600)
941
- _AWAIT_AUTH_MODE.pop(uid, None)
942
-
943
- if isinstance(out, dict) and out.get("ok") and out.get("login_url"):
944
- await safe_reply(
945
- m,
946
- f"✅ Profile added.\n\n🔗 Login URL:\n{out.get('login_url')}\n\nAfter login, run /profiles.",
947
- reply_markup=main_menu_keyboard(),
948
- )
949
- else:
950
- await safe_reply(m, f"❌ Add failed: `{(out or {}).get('err', 'unknown')}`", reply_markup=main_menu_keyboard())
951
- return
952
-
953
- # Edit title/desc
954
  if uid in _AWAIT_EDIT:
955
  _AWAIT_EDIT.pop(uid, None)
956
  p = _PENDING_UPLOAD.get(uid)
957
  if not p:
958
  return
959
-
960
  lines = txt.splitlines()
961
  if len(lines) == 1:
962
  p.title = lines[0].strip()[: Settings.MAX_TITLE]
963
  else:
964
- p.title = (lines[0] or "").strip()[: Settings.MAX_TITLE]
965
  p.description = "\n".join(lines[1:]).strip()[: Settings.MAX_DESC]
966
-
967
  _PENDING_UPLOAD[uid] = p
968
  _, _, size = _media_and_filename(p.src_msg)
969
- await safe_reply(m, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy))
970
  return
 
1
  # PATH: bot/handlers.py
 
 
 
2
  from __future__ import annotations
3
 
4
  import asyncio
 
11
  from hydrogram.types import CallbackQuery, Message
12
 
13
  from bot.config import Auth, Workers
 
 
 
14
  from bot.integrations.auth import allow_user, disallow_user, get_stats, is_allowed
15
+ from bot.integrations.cf_worker1 import profile_check_auth, profile_delete
16
+ from bot.integrations.cf_worker2 import get_default_profile, list_profiles, record_upload, set_default_profile
 
 
 
 
 
17
  from bot.telegram.files import cleanup_file
18
  from bot.telegram.media import download_to_temp
19
  from bot.telegram.replies import safe_edit, safe_reply
20
  from bot.ui.callbacks import (
 
 
 
 
21
  MENU_AUTH,
22
  MENU_HELP,
23
  MENU_PROFILES,
 
26
  UP_EDIT,
27
  UP_GO,
28
  UP_PRIV,
29
+ NAME_ORIGINAL,
30
+ NAME_CAPTION,
31
+ NAME_CUSTOM,
32
+ BACK,
33
+ UP_CANCEL,
34
  )
35
  from bot.ui.keyboards import auth_menu_keyboard, main_menu_keyboard, profiles_keyboard, upload_confirm_keyboard
36
  from bot.ui.parse import parse_cb
37
  from bot.ui.texts import CANCELLED, HELP_TEXT, NEED_AUTH, NOT_ALLOWED, OWNER_ONLY
38
+ from bot.core.progress import SpeedETA, human_bytes, human_eta
39
+ from bot.core.settings import Settings
40
+ from bot.core.uptime import uptime_text
41
  from bot.youtube.link_parser import parse_telegram_link
42
 
43
 
 
 
 
 
44
  @dataclass
45
  class PendingUpload:
46
  src_msg: Message
47
  downloader: Client
48
  file_name: str
49
+ caption: str
50
  title: str
51
  description: str
52
  privacy: str = "private"
53
+ title_mode: str = "caption" # filename | caption | custom
54
  status_msg: Optional[Message] = None
55
  via_link: bool = False
56
 
 
62
  privacy: str
63
 
64
 
65
+ _PENDING_UPLOAD: Dict[int, PendingUpload] = {}
66
+ _AWAIT_EDIT: Dict[int, EditState] = {}
67
+ _AWAIT_AUTH_MODE: Dict[int, str] = {}
68
+ _PENDING_DELETE: Dict[int, str] = {}
69
+ _IN_PROGRESS: Dict[int, bool] = {}
70
+ _UPLOAD_TASK: Dict[int, asyncio.Task] = {}
71
+ _BATCH_TASK: Dict[int, asyncio.Task] = {}
 
 
72
 
 
 
 
73
 
74
  def _is_admin_or_owner(uid: int) -> bool:
75
+ return uid in set(Auth.OWNERS) or uid in set(Auth.ADMINS)
76
 
77
 
78
+ async def _ensure_allowed_msg(m: Message) -> bool:
79
+ uid = m.from_user.id if m.from_user else 0
80
+ if _is_admin_or_owner(uid):
81
+ return True
82
+ ok = await is_allowed(uid)
83
+ if not ok:
84
+ await safe_reply(m, NOT_ALLOWED)
85
+ return ok
86
+
87
+
88
+ async def _ensure_allowed_cb(uid: int, msg_to_edit: Message) -> bool:
89
  """
90
+ IMPORTANT FIX:
91
+ CallbackQuery should check q.from_user.id (clicker), NOT q.message.from_user (bot).
92
  """
 
 
 
93
  if _is_admin_or_owner(uid):
94
  return True
 
95
  ok = await is_allowed(uid)
96
  if not ok:
97
+ await safe_edit(msg_to_edit, NOT_ALLOWED, reply_markup=main_menu_keyboard())
98
  return ok
99
 
100
 
 
106
  return None, "", 0
107
 
108
 
109
+ def _build_title_desc_from_mode(src: Message, file_name: str, mode: str) -> Tuple[str, str]:
110
+ cap = (src.caption or "").strip()
 
 
 
 
 
 
 
111
  base = os.path.splitext(file_name)[0].strip()
112
+ title_from_filename = (base or "Untitled")[: Settings.MAX_TITLE]
113
+
114
+ if cap:
115
+ parts = cap.splitlines()
116
+ title_from_caption = parts[0].strip()[: Settings.MAX_TITLE]
117
+ desc_from_caption = "\n".join([p.strip() for p in parts[1:]]).strip()[: Settings.MAX_DESC]
118
+ else:
119
+ title_from_caption = ""
120
+ desc_from_caption = ""
121
+
122
+ mode = (mode or "").lower().strip()
123
+
124
+ if mode == "filename":
125
+ # filename as title, keep full caption as description (useful)
126
+ return title_from_filename, cap[: Settings.MAX_DESC]
127
+ if mode == "caption":
128
+ if title_from_caption:
129
+ return title_from_caption, desc_from_caption
130
+ return title_from_filename, cap[: Settings.MAX_DESC]
131
+ # custom fallback
132
+ return title_from_filename, cap[: Settings.MAX_DESC]
133
 
134
 
135
  def _render_preview(p: PendingUpload, size: int) -> str:
136
+ cap = (p.caption or "").strip()
137
+ cap_short = (cap[:200] + "…") if len(cap) > 200 else (cap or "—")
138
+
139
+ mode = (p.title_mode or "caption").lower()
140
  return (
141
+ "📦 *Ready to upload*\n\n"
142
+ f"*File:* `{p.file_name}`\n"
143
+ f"*Size:* `{human_bytes(size)}`\n"
144
+ f"*Privacy:* `{p.privacy}`\n"
145
+ f"*Title source:* `{mode}`\n\n"
146
+ f"*Caption (preview):*\n`{cap_short}`\n\n"
147
+ f"*Current Title:*\n`{p.title}`\n\n"
148
+ f"*Current Description:*\n"
149
+ f"`{(p.description[:600] + '…') if len(p.description) > 600 else (p.description or '—')}`"
150
  )
151
 
152
 
153
+ async def _start_pending_upload(
154
+ *,
155
+ uid: int,
156
+ request_msg: Message, # where user asked (DM/group)
157
+ src: Message, # actual media message (maybe from another chat)
158
+ downloader: Client,
159
+ via_link: bool = False,
160
+ ui_msg: Optional[Message] = None, # if provided, edit this message instead of replying
161
+ ) -> None:
162
  media, file_name, size = _media_and_filename(src)
163
  if not media:
164
+ if ui_msg:
165
+ await safe_edit(ui_msg, "❌ No video/document found in that message.", reply_markup=main_menu_keyboard())
166
+ else:
167
+ await safe_reply(request_msg, "❌ No video/document found in that message.")
168
  return
169
 
170
+ caption = (src.caption or "").strip()
171
+
172
+ # default: prefer caption if present
173
+ default_mode = "caption" if caption else "filename"
174
+ title, desc = _build_title_desc_from_mode(src, file_name, default_mode)
175
 
176
  p = PendingUpload(
177
  src_msg=src,
178
  downloader=downloader,
179
  file_name=file_name,
180
+ caption=caption,
181
  title=title,
182
  description=desc,
183
  privacy="private",
184
+ title_mode=default_mode,
185
  via_link=via_link,
186
  status_msg=None,
187
  )
 
188
 
189
  txt = _render_preview(p, size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ # IMPORTANT FIX:
192
+ # Preview must appear in the request chat (DM/group), not inside the source chat.
193
+ if ui_msg:
194
+ p.status_msg = ui_msg
195
+ _PENDING_UPLOAD[uid] = p
196
+ await safe_edit(ui_msg, txt, reply_markup=upload_confirm_keyboard(p.privacy, p.title_mode))
197
+ return
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ st = await safe_reply(request_msg, txt, reply_markup=upload_confirm_keyboard(p.privacy, p.title_mode))
200
+ p.status_msg = st
201
+ _PENDING_UPLOAD[uid] = p
202
 
203
 
204
+ async def _run_upload(uid: int) -> Dict[str, Any]:
205
  if _IN_PROGRESS.get(uid):
206
+ return {"ok": False, "err": "already_running"}
207
+
208
  pending = _PENDING_UPLOAD.get(uid)
209
  if not pending:
210
+ return {"ok": False, "err": "no_pending"}
 
 
211
 
 
212
  st = pending.status_msg
213
+ if not st:
214
+ return {"ok": False, "err": "no_status_msg"}
215
+
216
+ _IN_PROGRESS[uid] = True
217
  file_path: Optional[str] = None
218
 
219
  try:
 
220
  prof = await get_default_profile(uid)
221
  if not (isinstance(prof, dict) and prof.get("ok") and prof.get("profile_id") and prof.get("access_token")):
222
  await safe_edit(st, NEED_AUTH, reply_markup=main_menu_keyboard())
223
+ return {"ok": False, "err": "need_auth"}
224
 
225
  prof_id = str(prof["profile_id"])
226
  access_token = str(prof["access_token"])
227
 
228
  overall_start = time.time()
229
 
230
+ def _as_int(v: Any) -> int:
231
+ if v is None:
232
+ return 0
233
+ if isinstance(v, bool):
234
+ return int(v)
235
+ if isinstance(v, (int, float)):
236
+ return int(v)
237
+ if isinstance(v, str):
238
+ try:
239
+ return int(float(v.strip()))
240
+ except Exception:
241
+ return 0
242
+ if isinstance(v, dict):
243
+ for k in ("current", "sent", "bytes", "done", "downloaded", "uploaded", "processed"):
244
+ if k in v:
245
+ try:
246
+ return int(float(v.get(k) or 0))
247
+ except Exception:
248
+ continue
249
+ try:
250
+ return int(float(next(iter(v.values()))))
251
+ except Exception:
252
+ return 0
253
+ return 0
254
+
255
+ # ---- Download (TG -> temp) ----
256
  await safe_edit(st, "⬇️ Downloading…")
257
  dl_speed = SpeedETA()
258
  dl_last_ui = 0.0
 
272
 
273
  rate = dl_speed.update(cur_i, total_i)
274
  txt = (
275
+ "⬇️ *Downloading…*\n\n"
276
+ f"`{human_bytes(cur_i)} / {human_bytes(total_i)}`\n"
277
+ f"*Speed:* `{human_bytes(rate)}/s`\n"
278
+ f"*ETA:* `{human_eta(dl_speed.eta_seconds)}`"
279
  )
280
  await safe_edit(st, txt)
281
 
282
+ file_path, _, _ = await download_to_temp(pending.downloader, pending.src_msg, progress_cb=dl_progress)
 
 
 
 
283
  dl_dur = max(0.001, time.time() - dl_start)
284
+
285
  try:
286
  file_bytes = int(os.path.getsize(file_path))
287
  except Exception:
288
  file_bytes = 0
289
 
290
+ # ---- Upload (temp -> YouTube) ----
291
+ from bot.youtube.uploader import upload_video
292
 
293
  ul_speed = SpeedETA()
294
  ul_last_ui = 0.0
 
308
 
309
  rate = ul_speed.update(sent_i, total_i)
310
  txt = (
311
+ "⬆️ *Uploading…*\n\n"
312
+ f"`{human_bytes(sent_i)} / {human_bytes(total_i)}`\n"
313
+ f"*Speed:* `{human_bytes(rate)}/s`\n"
314
+ f"*ETA:* `{human_eta(ul_speed.eta_seconds)}`"
315
  )
316
  await safe_edit(st, txt)
317
 
 
333
  d = str(detail)
334
  msg += f"\n`{d[:280]}`"
335
  if "uploadLimitExceeded" in d or "quotaExceeded" in d:
336
+ msg += "\n\nℹ️ Daily quota limit. Try another profile or retry later."
337
 
338
  await safe_edit(st, msg, reply_markup=main_menu_keyboard())
339
+ return {"ok": False, "err": err, "detail": detail}
 
 
340
 
341
+ url = up.get("url") or ""
342
  await record_upload(uid, prof_id)
343
 
344
  ul_dur = max(0.001, time.time() - ul_start)
345
  total_dur = max(0.001, time.time() - overall_start)
346
 
347
+ dl_avg = (human_bytes(file_bytes / dl_dur) + "/s") if file_bytes else "—"
348
+ ul_avg = (human_bytes(file_bytes / ul_dur) + "/s") if file_bytes else "—"
349
 
350
+ done = "✅ *Uploaded!*"
351
  if url:
352
  done += f"\n\n{url}"
353
  done += (
354
+ f"\n\n*Download:* `{dl_dur:.1f}s` • avg `{dl_avg}`"
355
+ f"\n*Upload:* `{ul_dur:.1f}s` • avg `{ul_avg}`"
356
+ f"\n*Total:* `{total_dur:.1f}s`"
357
  )
358
  await safe_edit(st, done, reply_markup=main_menu_keyboard())
359
+ return {"ok": True, "url": url}
360
 
361
  except asyncio.CancelledError:
362
  try:
363
  await safe_edit(st, CANCELLED, reply_markup=main_menu_keyboard())
364
  except Exception:
365
  pass
366
+ return {"ok": False, "err": "cancelled"}
367
+
368
  except Exception as e:
369
  await safe_edit(st, f"❌ Error: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
370
+ return {"ok": False, "err": "exception", "detail": str(e)}
371
+
372
  finally:
373
  _PENDING_UPLOAD.pop(uid, None)
374
  _AWAIT_EDIT.pop(uid, None)
 
381
  _UPLOAD_TASK.pop(uid, None)
382
 
383
 
 
 
 
 
384
  def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
385
+ @app.on_message(filters.command(["start", "help"]))
 
386
  async def start_cmd(_, m: Message) -> None:
387
  from bot.ui.texts import START_TEXT
388
  await safe_reply(m, START_TEXT, reply_markup=main_menu_keyboard())
389
 
390
+ @app.on_message(filters.command("cancel"))
391
  async def cancel_cmd(_, m: Message) -> None:
392
  uid = m.from_user.id
 
393
  _AWAIT_AUTH_MODE.pop(uid, None)
394
  _AWAIT_EDIT.pop(uid, None)
395
  _PENDING_UPLOAD.pop(uid, None)
 
404
  btask.cancel()
405
 
406
  _IN_PROGRESS.pop(uid, None)
 
407
  await safe_reply(m, CANCELLED, reply_markup=main_menu_keyboard())
408
 
409
+ @app.on_message(filters.command("allow"))
 
410
  async def allow_cmd(_, m: Message) -> None:
411
  uid = m.from_user.id
412
  if not _is_admin_or_owner(uid):
413
  await safe_reply(m, OWNER_ONLY)
414
  return
 
415
  args = (m.text or "").split(maxsplit=1)
416
  if len(args) < 2:
417
  await safe_reply(m, "Usage: /allow <tg_id>")
 
421
  except Exception:
422
  await safe_reply(m, "Invalid tg_id")
423
  return
 
424
  await allow_user(tid)
425
  await safe_reply(m, f"✅ Allowed `{tid}`")
426
 
427
+ @app.on_message(filters.command("disallow"))
428
  async def disallow_cmd(_, m: Message) -> None:
429
  uid = m.from_user.id
430
  if not _is_admin_or_owner(uid):
431
  await safe_reply(m, OWNER_ONLY)
432
  return
 
433
  args = (m.text or "").split(maxsplit=1)
434
  if len(args) < 2:
435
  await safe_reply(m, "Usage: /disallow <tg_id>")
 
439
  except Exception:
440
  await safe_reply(m, "Invalid tg_id")
441
  return
 
442
  await disallow_user(tid)
443
  await safe_reply(m, f"✅ Disallowed `{tid}`")
444
 
445
+ @app.on_message(filters.command("stats"))
446
  async def stats_cmd(_, m: Message) -> None:
447
  uid = m.from_user.id
448
  if not _is_admin_or_owner(uid):
449
  await safe_reply(m, OWNER_ONLY)
450
  return
 
451
  st = await get_stats()
452
  if not isinstance(st, dict):
453
  await safe_reply(m, "❌ stats failed")
454
  return
 
455
  await safe_reply(
456
  m,
457
  "📊 *Today stats*\n\n"
 
460
  f"Uploads today: `{st.get('uploads_today', 0)}`",
461
  )
462
 
463
+ @app.on_message(filters.command("diag"))
464
  async def diag_cmd(_, m: Message) -> None:
465
  uid = m.from_user.id
466
  if not _is_admin_or_owner(uid):
 
479
  f"Owners: `{len(Auth.OWNERS)}` Admins: `{len(Auth.ADMINS)}`",
480
  )
481
 
482
+ @app.on_message(filters.command("auth"))
 
483
  async def auth_cmd(_, m: Message) -> None:
484
+ if not await _ensure_allowed_msg(m):
485
  return
486
+ uid = m.from_user.id
487
+ _AWAIT_AUTH_MODE[uid] = "oauth"
488
  await safe_reply(m, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
489
 
490
+ @app.on_message(filters.command("profiles"))
491
  async def profiles_cmd(_, m: Message) -> None:
492
+ if not await _ensure_allowed_msg(m):
493
  return
494
  uid = m.from_user.id
495
  data = await list_profiles(uid, only_connected=False)
 
500
  default_id = data.get("default_profile_id") or ""
501
  await safe_reply(m, "👤 *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
502
 
503
+ # accept media in any chat (DM/group) - still gated by allowed
504
+ @app.on_message((filters.video | filters.document))
505
+ async def media_in_chat(_, m: Message) -> None:
506
+ if not await _ensure_allowed_msg(m):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  return
508
+ uid = m.from_user.id
509
  if _IN_PROGRESS.get(uid):
510
  await safe_reply(m, "⏳ Upload already running. Use /cancel to stop.")
511
  return
512
 
513
+ await _start_pending_upload(uid=uid, request_msg=m, src=m, downloader=app, via_link=False, ui_msg=None)
514
 
515
+ # /yt /dl /archive in ANY chat; preview + progress stays in request chat
516
+ @app.on_message(filters.command(["yt", "dl", "archive"]))
517
  async def archive_cmd(_, m: Message) -> None:
518
  uid = m.from_user.id
519
+ if not await _ensure_allowed_msg(m):
520
  return
521
  if not _is_admin_or_owner(uid):
522
  await safe_reply(m, OWNER_ONLY)
 
527
 
528
  args = (m.text or "").split(maxsplit=1)
529
  if len(args) < 2:
530
+ await safe_reply(m, "Usage: /yt <t.me message link>")
531
+ return
532
+
533
+ st = await safe_reply(m, "🔎 Fetching message…")
534
+ if not st:
535
  return
536
 
537
  try:
538
  chat_ref, msg_id = parse_telegram_link(args[1].strip())
539
  except Exception as e:
540
+ await safe_edit(st, f"Bad link: {e}", reply_markup=main_menu_keyboard())
 
 
 
 
541
  return
542
 
543
  try:
 
546
  await safe_edit(st, f"❌ Fetch failed: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
547
  return
548
 
549
+ await safe_edit(st, "✅ Message fetched. Preparing preview…")
550
+ await _start_pending_upload(uid=uid, request_msg=m, src=src, downloader=user_app, via_link=True, ui_msg=st)
551
 
552
+ # batch sequential, stop on first failure by default
553
+ @app.on_message(filters.command("batch"))
554
  async def batch_cmd(_, m: Message) -> None:
555
+ if not await _ensure_allowed_msg(m):
556
  return
557
  uid = m.from_user.id
558
  if not _is_admin_or_owner(uid):
 
576
  await safe_reply(m, "⏳ A batch is already running. Use /cancel to stop it.")
577
  return
578
 
579
+ await safe_reply(m, f"🧾 Batch starting: {len(links)} item(s). (Sequential queue)")
580
 
581
  async def runner() -> None:
582
  batch_start = time.time()
583
  for i, link in enumerate(links, 1):
 
 
 
 
 
 
584
  st = await safe_reply(m, f"🔎 Batch {i}/{len(links)}: fetching…")
585
  if not st:
586
  continue
587
 
588
+ try:
589
+ chat_ref, msg_id = parse_telegram_link(link)
590
+ except Exception as e:
591
+ await safe_edit(st, f"❌ Batch {i}/{len(links)} bad link: {e}", reply_markup=main_menu_keyboard())
592
+ return
593
+
594
  try:
595
  src = await user_app.get_messages(chat_ref, msg_id)
596
  except Exception as e:
597
  await safe_edit(st, f"❌ Batch {i}/{len(links)} fetch failed: `{str(e)[:180]}`", reply_markup=main_menu_keyboard())
598
+ return
599
 
600
+ await safe_edit(st, f"✅ Batch {i}/{len(links)}: preview…")
601
+ await _start_pending_upload(uid=uid, request_msg=m, src=src, downloader=user_app, via_link=True, ui_msg=st)
 
 
602
 
603
+ await safe_edit(st, f"⏳ Batch {i}/{len(links)}: starting upload…")
604
+ res = await _run_upload(uid)
 
 
 
 
 
 
 
 
 
 
605
 
606
+ if not (isinstance(res, dict) and res.get("ok")):
607
+ # STOP ON FAILURE (your requested behavior)
608
+ await safe_reply(m, f"🛑 Batch stopped at item {i}/{len(links)} due to failure.")
609
+ return
610
 
611
  batch_dur = max(0.001, time.time() - batch_start)
612
+ await safe_reply(m, f"✅ Batch done in {batch_dur:.1f}s.")
613
 
614
  t = asyncio.create_task(runner())
615
  _BATCH_TASK[uid] = t
616
 
617
+ @app.on_callback_query()
 
 
 
618
  async def cb_handler(_, q: CallbackQuery) -> None:
619
+ uid = q.from_user.id
 
 
 
 
 
 
620
  action, value = parse_cb(q.data or "")
621
 
 
622
  if action == MENU_HELP:
623
  await safe_edit(q.message, HELP_TEXT, reply_markup=main_menu_keyboard())
624
  return
625
 
 
626
  if action == BACK:
627
+ from bot.ui.texts import START_TEXT
628
+ await safe_edit(q.message, START_TEXT, reply_markup=main_menu_keyboard())
629
  return
630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  if action == MENU_SPEED:
632
+ if not await _ensure_allowed_cb(uid, q.message):
633
  return
634
+ from bot.core.speedtest import net_download_test, net_upload_test, public_ip, ping_ms
635
+ await safe_edit(q.message, "⏱ Running speed test…")
636
+
637
+ ip = await public_ip()
638
+ ping = await ping_ms()
639
 
 
640
  dl = await net_download_test()
641
  ul = await net_upload_test()
 
 
 
 
 
 
 
 
642
 
643
+ dl_bps = float((dl or {}).get("bps", 0) or 0)
644
+ ul_bps = float((ul or {}).get("bps", 0) or 0)
645
+
646
+ lines = [
647
+ "📶 *Speed Test*",
648
+ f"Uptime: `{uptime_text()}`",
649
+ ]
650
+ if ip:
651
+ lines.append(f"Public IP: `{ip}`")
652
+ if ping is not None:
653
+ lines.append(f"Ping(443): `{ping:.0f} ms`")
654
+
655
+ lines.append("")
656
+ lines.append(f"Download: `{human_bytes(dl_bps)}/s`")
657
+ lines.append(f"Upload: `{human_bytes(ul_bps)}/s`")
658
+
659
+ await safe_edit(q.message, "\n".join(lines), reply_markup=main_menu_keyboard())
660
+ return
661
+
662
+ if action == MENU_AUTH:
663
+ if not await _ensure_allowed_cb(uid, q.message):
664
+ return
665
+ await safe_edit(q.message, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
666
  return
667
 
668
  if action == MENU_PROFILES:
669
+ if not await _ensure_allowed_cb(uid, q.message):
670
  return
671
  data = await list_profiles(uid, only_connected=False)
672
  if not (isinstance(data, dict) and data.get("ok")):
 
677
  await safe_edit(q.message, "👤 *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
678
  return
679
 
680
+ # title-source buttons
681
+ if action in (NAME_ORIGINAL, NAME_CAPTION, NAME_CUSTOM):
682
+ p = _PENDING_UPLOAD.get(uid)
683
+ if not p:
684
+ return
685
+
686
+ if action == NAME_ORIGINAL:
687
+ p.title_mode = "filename"
688
+ p.title, p.description = _build_title_desc_from_mode(p.src_msg, p.file_name, "filename")
689
+ elif action == NAME_CAPTION:
690
+ p.title_mode = "caption"
691
+ p.title, p.description = _build_title_desc_from_mode(p.src_msg, p.file_name, "caption")
692
+ else:
693
+ # custom -> reuse edit flow
694
+ p.title_mode = "custom"
695
+ _AWAIT_EDIT[uid] = EditState(title=p.title, description=p.description, privacy=p.privacy)
696
+ _PENDING_UPLOAD[uid] = p
697
+ await safe_edit(
698
+ q.message,
699
+ "✍️ Send new title + description like this:\n\n"
700
+ "Title line\n"
701
+ "Description lines…\n\n"
702
+ "(Send only 1 line to change title only)",
703
+ )
704
+ return
705
+
706
+ _PENDING_UPLOAD[uid] = p
707
+ media, _, size = _media_and_filename(p.src_msg)
708
+ await safe_edit(q.message, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy, p.title_mode))
709
  return
710
 
 
711
  if action == UP_PRIV:
712
  p = _PENDING_UPLOAD.get(uid)
713
  if not p:
 
716
  p.privacy = cycle.get(p.privacy, "private")
717
  _PENDING_UPLOAD[uid] = p
718
  _, _, size = _media_and_filename(p.src_msg)
719
+ await safe_edit(q.message, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy, p.title_mode))
720
  return
721
 
722
  if action == UP_EDIT:
723
  p = _PENDING_UPLOAD.get(uid)
724
  if not p:
725
  return
726
+ p.title_mode = "custom"
727
  _AWAIT_EDIT[uid] = EditState(title=p.title, description=p.description, privacy=p.privacy)
728
+ _PENDING_UPLOAD[uid] = p
729
  await safe_edit(
730
  q.message,
731
  "✍️ Send new title + description like this:\n\n"
732
  "Title line\n"
733
  "Description lines…\n\n"
734
  "(Send only 1 line to change title only)",
 
735
  )
736
  return
737
 
738
+ if action in (UP_DEL, UP_CANCEL):
739
  _PENDING_UPLOAD.pop(uid, None)
740
  await safe_edit(q.message, CANCELLED, reply_markup=main_menu_keyboard())
741
  return
 
749
  return
750
  p.status_msg = q.message
751
  _PENDING_UPLOAD[uid] = p
752
+ await safe_edit(q.message, "⏳ Starting upload…")
753
  t = asyncio.create_task(_run_upload(uid))
754
  _UPLOAD_TASK[uid] = t
755
  return
756
 
757
+ # profile actions
758
  if action == "pdel":
759
+ if not await _ensure_allowed_cb(uid, q.message):
760
  return
761
  pid = value
762
  _PENDING_DELETE[uid] = pid
763
+ await safe_edit(q.message, f"❓ Delete profile `{pid}`? Reply YES to confirm.")
764
  return
765
 
766
  if action == "pdef":
767
+ if not await _ensure_allowed_cb(uid, q.message):
768
  return
769
  pid = value
770
  out = await set_default_profile(uid, pid)
 
778
  return
779
 
780
  if action == "plog":
781
+ if not await _ensure_allowed_cb(uid, q.message):
782
  return
783
  pid = value
784
  chk = await profile_check_auth(uid, pid)
 
792
  await safe_edit(q.message, "❌ Failed to create login URL.", reply_markup=main_menu_keyboard())
793
  return
794
 
795
+ @app.on_message(filters.text)
796
+ async def text_in_chat(_, m: Message) -> None:
797
+ uid = m.from_user.id if m.from_user else 0
 
 
 
 
 
 
 
798
 
 
799
  if uid in _PENDING_DELETE:
800
+ if (m.text or "").strip().lower() == "yes":
801
  pid = _PENDING_DELETE.pop(uid)
802
  out = await profile_delete(uid, pid)
803
  if isinstance(out, dict) and out.get("ok"):
 
809
  await safe_reply(m, "Cancelled.", reply_markup=main_menu_keyboard())
810
  return
811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  if uid in _AWAIT_EDIT:
813
  _AWAIT_EDIT.pop(uid, None)
814
  p = _PENDING_UPLOAD.get(uid)
815
  if not p:
816
  return
817
+ txt = (m.text or "").strip()
818
  lines = txt.splitlines()
819
  if len(lines) == 1:
820
  p.title = lines[0].strip()[: Settings.MAX_TITLE]
821
  else:
822
+ p.title = lines[0].strip()[: Settings.MAX_TITLE]
823
  p.description = "\n".join(lines[1:]).strip()[: Settings.MAX_DESC]
824
+ p.title_mode = "custom"
825
  _PENDING_UPLOAD[uid] = p
826
  _, _, size = _media_and_filename(p.src_msg)
827
+ await safe_reply(m, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy, p.title_mode))
828
  return