understanding commited on
Commit
eb339a4
·
verified ·
1 Parent(s): 6b6c6e6

Update bot/handlers.py

Browse files
Files changed (1) hide show
  1. bot/handlers.py +358 -122
bot/handlers.py CHANGED
@@ -1,10 +1,6 @@
1
- # FILE: bot/handlers.py
2
- # NOTE: Updated:
3
- # - Fix float(dict) crash by using SpeedETA that returns float and safe int conversions
4
- # - Show download speed/ETA + upload speed/ETA + total time summary
5
- # - Fix upload_video return-type mismatch (now dict)
6
- # - /cancel actually cancels running upload and batch tasks
7
- # - Add /batch (admin/owner only)
8
 
9
  from __future__ import annotations
10
 
@@ -15,30 +11,47 @@ from dataclasses import dataclass
15
  from typing import Any, Dict, Optional, Tuple
16
 
17
  from hydrogram import Client, filters
18
- from hydrogram.types import CallbackQuery, InlineKeyboardMarkup, Message
19
 
20
  from bot.config import Auth, Workers
21
- from bot.integrations.auth import (allow_user, disallow_user, get_stats,
22
- is_allowed)
23
- from bot.integrations.cf_worker1 import (profile_add, profile_check_auth,
24
- profile_delete)
25
- from bot.integrations.cf_worker2 import (get_default_profile, list_profiles,
26
- record_upload, set_default_profile)
 
 
 
 
 
27
  from bot.telegram.files import cleanup_file
28
  from bot.telegram.media import download_to_temp
29
  from bot.telegram.replies import safe_edit, safe_reply
30
- from bot.ui.callbacks import (MENU_AUTH, MENU_HELP, MENU_PROFILES, MENU_SPEED,
31
- UP_DEL, UP_EDIT, UP_GO, UP_PRIV)
32
- from bot.ui.keyboards import (auth_menu_keyboard, main_menu_keyboard,
33
- profiles_keyboard, upload_confirm_keyboard)
 
 
 
 
 
 
 
 
 
 
 
34
  from bot.ui.parse import parse_cb
35
  from bot.ui.texts import CANCELLED, HELP_TEXT, NEED_AUTH, NOT_ALLOWED, OWNER_ONLY
36
- from bot.core.progress import SpeedETA, human_bytes, human_eta
37
- from bot.core.settings import Settings
38
- from bot.core.uptime import uptime_text
39
  from bot.youtube.link_parser import parse_telegram_link
40
 
41
 
 
 
 
 
42
  @dataclass
43
  class PendingUpload:
44
  src_msg: Message
@@ -59,26 +72,37 @@ class EditState:
59
 
60
 
61
  # In-memory state
62
- _PENDING_UPLOAD: Dict[int, PendingUpload] = {} # uid -> pending upload
63
- _AWAIT_EDIT: Dict[int, EditState] = {} # uid -> editing title/desc
64
- _AWAIT_AUTH_MODE: Dict[int, str] = {} # uid -> "oauth" | "token"
65
- _PENDING_DELETE: Dict[int, str] = {} # uid -> profile_id to delete
66
- _IN_PROGRESS: Dict[int, bool] = {} # uid -> upload is running
67
- _UPLOAD_TASK: Dict[int, asyncio.Task] = {} # uid -> running upload task (so /cancel can stop it)
68
- _BATCH_TASK: Dict[int, asyncio.Task] = {} # uid -> running batch task
 
69
 
 
 
 
70
 
71
  def _is_admin_or_owner(uid: int) -> bool:
72
- return uid in set(Auth.OWNERS) or uid in set(Auth.ADMINS)
73
 
74
 
75
- async def _ensure_allowed(m: Message) -> bool:
76
- uid = m.from_user.id
 
 
 
 
 
 
77
  if _is_admin_or_owner(uid):
78
  return True
 
79
  ok = await is_allowed(uid)
80
  if not ok:
81
- await safe_reply(m, NOT_ALLOWED)
82
  return ok
83
 
84
 
@@ -94,25 +118,24 @@ def extract_title_description(src: Message, file_name: str) -> Tuple[str, str]:
94
  # Prefer caption text
95
  caption = (src.caption or "").strip()
96
  if caption:
97
- # title = first line, desc = remaining lines
98
  parts = caption.splitlines()
99
- title = parts[0].strip()
100
  desc = "\n".join([p.strip() for p in parts[1:]]).strip()
101
- return title[:Settings.MAX_TITLE], desc[:Settings.MAX_DESC]
102
- # fallback to filename
103
  base = os.path.splitext(file_name)[0].strip()
104
- title = (base or "Untitled")[:Settings.MAX_TITLE]
105
  return title, ""
106
 
107
 
108
  def _render_preview(p: PendingUpload, size: int) -> str:
109
  return (
110
- "📦 *Ready to upload*\\n\\n"
111
- f"*File:* `{p.file_name}`\\n"
112
- f"*Size:* {human_bytes(size)}\\n"
113
- f"*Privacy:* `{p.privacy}`\\n\\n"
114
- f"*Title:* {p.title}\\n"
115
- f"*Description:* {(p.description[:600] + '…') if len(p.description) > 600 else (p.description or '—')}"
116
  )
117
 
118
 
@@ -139,6 +162,54 @@ async def _start_pending_upload(uid: int, src: Message, downloader: Client, via_
139
  await safe_reply(src, txt, reply_markup=upload_confirm_keyboard(p.privacy))
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  async def _run_upload(uid: int) -> None:
143
  if _IN_PROGRESS.get(uid):
144
  return
@@ -162,37 +233,8 @@ async def _run_upload(uid: int) -> None:
162
  prof_id = str(prof["profile_id"])
163
  access_token = str(prof["access_token"])
164
 
165
- # Track total time (download + upload)
166
  overall_start = time.time()
167
 
168
- # Safety: Telegram/hydrogram callbacks sometimes pass dict/None.
169
- # Convert to int to avoid int(dict)/float(dict) crashes.
170
- def _as_int(v: Any) -> int:
171
- if v is None:
172
- return 0
173
- if isinstance(v, bool):
174
- return int(v)
175
- if isinstance(v, (int, float)):
176
- return int(v)
177
- if isinstance(v, str):
178
- try:
179
- return int(float(v.strip()))
180
- except Exception:
181
- return 0
182
- if isinstance(v, dict):
183
- for k in ("current", "sent", "bytes", "done", "downloaded", "uploaded", "processed"):
184
- if k in v:
185
- try:
186
- return int(float(v.get(k) or 0))
187
- except Exception:
188
- continue
189
- # common pattern: {"current":..., "total":...}
190
- try:
191
- return int(float(next(iter(v.values()))))
192
- except Exception:
193
- return 0
194
- return 0
195
-
196
  # ---- Download (TG -> temp) with live speed ----
197
  await safe_edit(st, "⬇️ Downloading…")
198
  dl_speed = SpeedETA()
@@ -213,14 +255,18 @@ async def _run_upload(uid: int) -> None:
213
 
214
  rate = dl_speed.update(cur_i, total_i)
215
  txt = (
216
- "⬇️ Downloading…\\n\\n"
217
- f"{human_bytes(cur_i)} / {human_bytes(total_i)}\\n"
218
- f"Speed: {human_bytes(rate)}/s\\n"
219
  f"ETA: {human_eta(dl_speed.eta_seconds)}"
220
  )
221
  await safe_edit(st, txt)
222
 
223
- file_path, _, _ = await download_to_temp(pending.downloader, pending.src_msg, progress_cb=dl_progress)
 
 
 
 
224
  dl_dur = max(0.001, time.time() - dl_start)
225
  try:
226
  file_bytes = int(os.path.getsize(file_path))
@@ -229,6 +275,7 @@ async def _run_upload(uid: int) -> None:
229
 
230
  # ---- Upload (temp -> YouTube) with live speed ----
231
  from bot.youtube.uploader import upload_video # local import
 
232
  ul_speed = SpeedETA()
233
  ul_last_ui = 0.0
234
  ul_start = time.time()
@@ -247,9 +294,9 @@ async def _run_upload(uid: int) -> None:
247
 
248
  rate = ul_speed.update(sent_i, total_i)
249
  txt = (
250
- "⬆️ Uploading…\\n\\n"
251
- f"{human_bytes(sent_i)} / {human_bytes(total_i)}\\n"
252
- f"Speed: {human_bytes(rate)}/s\\n"
253
  f"ETA: {human_eta(ul_speed.eta_seconds)}"
254
  )
255
  await safe_edit(st, txt)
@@ -271,14 +318,13 @@ async def _run_upload(uid: int) -> None:
271
  if detail:
272
  d = str(detail)
273
  msg += f"\n`{d[:280]}`"
274
-
275
  if "uploadLimitExceeded" in d or "quotaExceeded" in d:
276
  msg += "\n\nℹ️ This looks like a YouTube daily upload/quota limit. Try another profile or wait and retry later."
277
 
278
- await safe_edit(st, msg)
279
  return
280
 
281
- url = up.get("url") or ""
282
 
283
  await record_upload(uid, prof_id)
284
 
@@ -296,16 +342,15 @@ async def _run_upload(uid: int) -> None:
296
  f"\nUpload: {ul_dur:.1f}s • avg {ul_avg}"
297
  f"\nTotal: {total_dur:.1f}s"
298
  )
299
- await safe_edit(st, done)
300
 
301
  except asyncio.CancelledError:
302
- # /cancel pressed
303
  try:
304
  await safe_edit(st, CANCELLED, reply_markup=main_menu_keyboard())
305
  except Exception:
306
  pass
307
  except Exception as e:
308
- await safe_edit(st, f"❌ Error: `{str(e)[:180]}`")
309
  finally:
310
  _PENDING_UPLOAD.pop(uid, None)
311
  _AWAIT_EDIT.pop(uid, None)
@@ -318,6 +363,10 @@ async def _run_upload(uid: int) -> None:
318
  _UPLOAD_TASK.pop(uid, None)
319
 
320
 
 
 
 
 
321
  def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
322
  # ---- basic ----
323
  @app.on_message(filters.command(["start", "help"]) & filters.private)
@@ -328,12 +377,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
328
  @app.on_message(filters.command("cancel") & filters.private)
329
  async def cancel_cmd(_, m: Message) -> None:
330
  uid = m.from_user.id
 
331
  _AWAIT_AUTH_MODE.pop(uid, None)
332
  _AWAIT_EDIT.pop(uid, None)
333
  _PENDING_UPLOAD.pop(uid, None)
334
  _PENDING_DELETE.pop(uid, None)
335
 
336
- # stop running upload task (if any)
337
  task = _UPLOAD_TASK.pop(uid, None)
338
  if task and not task.done():
339
  task.cancel()
@@ -353,6 +402,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
353
  if not _is_admin_or_owner(uid):
354
  await safe_reply(m, OWNER_ONLY)
355
  return
 
356
  args = (m.text or "").split(maxsplit=1)
357
  if len(args) < 2:
358
  await safe_reply(m, "Usage: /allow <tg_id>")
@@ -362,6 +412,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
362
  except Exception:
363
  await safe_reply(m, "Invalid tg_id")
364
  return
 
365
  await allow_user(tid)
366
  await safe_reply(m, f"✅ Allowed `{tid}`")
367
 
@@ -371,6 +422,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
371
  if not _is_admin_or_owner(uid):
372
  await safe_reply(m, OWNER_ONLY)
373
  return
 
374
  args = (m.text or "").split(maxsplit=1)
375
  if len(args) < 2:
376
  await safe_reply(m, "Usage: /disallow <tg_id>")
@@ -380,6 +432,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
380
  except Exception:
381
  await safe_reply(m, "Invalid tg_id")
382
  return
 
383
  await disallow_user(tid)
384
  await safe_reply(m, f"✅ Disallowed `{tid}`")
385
 
@@ -389,10 +442,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
389
  if not _is_admin_or_owner(uid):
390
  await safe_reply(m, OWNER_ONLY)
391
  return
 
392
  st = await get_stats()
393
  if not isinstance(st, dict):
394
  await safe_reply(m, "❌ stats failed")
395
  return
 
396
  await safe_reply(
397
  m,
398
  "📊 *Today stats*\n\n"
@@ -425,8 +480,6 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
425
  async def auth_cmd(_, m: Message) -> None:
426
  if not await _ensure_allowed(m):
427
  return
428
- uid = m.from_user.id
429
- _AWAIT_AUTH_MODE[uid] = "oauth"
430
  await safe_reply(m, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
431
 
432
  @app.on_message(filters.command("profiles") & filters.private)
@@ -445,12 +498,56 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
445
  # ---- upload from DM media ----
446
  @app.on_message(filters.private & (filters.video | filters.document))
447
  async def media_in_dm(_, m: Message) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  if not await _ensure_allowed(m):
449
  return
450
- uid = m.from_user.id
451
  if _IN_PROGRESS.get(uid):
452
  await safe_reply(m, "⏳ Upload already running. Use /cancel to stop.")
453
  return
 
454
  await _start_pending_upload(uid, m, app, via_link=False)
455
 
456
  # ---- upload from link (admin/owner) ----
@@ -465,24 +562,29 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
465
  if user_app is None:
466
  await safe_reply(m, "❌ This mode requires user session (user_app missing).")
467
  return
 
468
  args = (m.text or "").split(maxsplit=1)
469
  if len(args) < 2:
470
  await safe_reply(m, "Usage: /archive <t.me message link>")
471
  return
 
472
  try:
473
  chat_ref, msg_id = parse_telegram_link(args[1].strip())
474
  except Exception as e:
475
  await safe_reply(m, f"Bad link: {e}")
476
  return
 
477
  st = await safe_reply(m, "🔎 Fetching message…")
478
  if not st:
479
  return
 
480
  try:
481
  src = await user_app.get_messages(chat_ref, msg_id)
482
  except Exception as e:
483
- await safe_edit(st, f"❌ Fetch failed: `{str(e)[:180]}`")
484
  return
485
- await safe_edit(st, "✅ Message fetched. Preparing preview…")
 
486
  await _start_pending_upload(uid, src, user_app, via_link=True)
487
 
488
  # batch mode (owner/admin only): /batch <t.me links...> (one per line)
@@ -530,12 +632,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
530
  try:
531
  src = await user_app.get_messages(chat_ref, msg_id)
532
  except Exception as e:
533
- await safe_edit(st, f"❌ Batch {i}/{len(links)} fetch failed: `{str(e)[:180]}`")
534
  continue
535
 
536
  media, file_name, _ = _media_and_filename(src)
537
  if not media:
538
- await safe_edit(st, f"❌ Batch {i}/{len(links)}: no video/document in that message.")
539
  continue
540
 
541
  title, desc = extract_title_description(src, file_name)
@@ -551,30 +653,90 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
551
  via_link=True,
552
  )
553
 
554
- await safe_edit(st, f"⏳ Batch {i}/{len(links)}: starting upload…")
555
  await _run_upload(uid)
556
 
557
  batch_dur = max(0.001, time.time() - batch_start)
558
- await safe_reply(m, f"✅ Batch done in {batch_dur:.1f}s.")
559
 
560
  t = asyncio.create_task(runner())
561
  _BATCH_TASK[uid] = t
562
 
563
- # callbacks
 
 
564
  @app.on_callback_query(filters.private)
565
  async def cb_handler(_, q: CallbackQuery) -> None:
566
- uid = q.from_user.id
 
 
 
 
 
 
567
  action, value = parse_cb(q.data or "")
568
- # Menus
 
569
  if action == MENU_HELP:
570
  await safe_edit(q.message, HELP_TEXT, reply_markup=main_menu_keyboard())
571
  return
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  if action == MENU_SPEED:
574
- if not await _ensure_allowed(q.message):
575
  return
576
  from bot.core.speedtest import net_download_test, net_upload_test
577
- await safe_edit(q.message, "⏱ Running speed test…")
 
578
  dl = await net_download_test()
579
  ul = await net_upload_test()
580
  try:
@@ -595,14 +757,8 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
595
  )
596
  return
597
 
598
- if action == MENU_AUTH:
599
- if not await _ensure_allowed(q.message):
600
- return
601
- await safe_edit(q.message, "🔐 Add a profile:", reply_markup=auth_menu_keyboard())
602
- return
603
-
604
  if action == MENU_PROFILES:
605
- if not await _ensure_allowed(q.message):
606
  return
607
  data = await list_profiles(uid, only_connected=False)
608
  if not (isinstance(data, dict) and data.get("ok")):
@@ -613,6 +769,10 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
613
  await safe_edit(q.message, "👤 *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
614
  return
615
 
 
 
 
 
616
  # Upload buttons
617
  if action == UP_PRIV:
618
  p = _PENDING_UPLOAD.get(uid)
@@ -621,7 +781,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
621
  cycle = {"private": "unlisted", "unlisted": "public", "public": "private"}
622
  p.privacy = cycle.get(p.privacy, "private")
623
  _PENDING_UPLOAD[uid] = p
624
- media, _, size = _media_and_filename(p.src_msg)
625
  await safe_edit(q.message, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy))
626
  return
627
 
@@ -636,6 +796,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
636
  "Title line\n"
637
  "Description lines…\n\n"
638
  "(Send only 1 line to change title only)",
 
639
  )
640
  return
641
 
@@ -653,22 +814,22 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
653
  return
654
  p.status_msg = q.message
655
  _PENDING_UPLOAD[uid] = p
656
- await safe_edit(q.message, "⏳ Starting upload…")
657
  t = asyncio.create_task(_run_upload(uid))
658
  _UPLOAD_TASK[uid] = t
659
  return
660
 
661
- # Profile callbacks (from profiles_keyboard)
662
  if action == "pdel":
663
- if not await _ensure_allowed(q.message):
664
  return
665
  pid = value
666
  _PENDING_DELETE[uid] = pid
667
- await safe_edit(q.message, f"❓ Delete profile `{pid}`? Reply YES to confirm.")
668
  return
669
 
670
  if action == "pdef":
671
- if not await _ensure_allowed(q.message):
672
  return
673
  pid = value
674
  out = await set_default_profile(uid, pid)
@@ -682,7 +843,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
682
  return
683
 
684
  if action == "plog":
685
- if not await _ensure_allowed(q.message):
686
  return
687
  pid = value
688
  chk = await profile_check_auth(uid, pid)
@@ -696,14 +857,20 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
696
  await safe_edit(q.message, "❌ Failed to create login URL.", reply_markup=main_menu_keyboard())
697
  return
698
 
699
- # ---- edit text handler ----
 
 
 
 
 
700
  @app.on_message(filters.private & filters.text)
701
  async def text_in_dm(_, m: Message) -> None:
702
  uid = m.from_user.id
 
703
 
704
  # Confirm delete
705
  if uid in _PENDING_DELETE:
706
- if (m.text or "").strip().lower() == "yes":
707
  pid = _PENDING_DELETE.pop(uid)
708
  out = await profile_delete(uid, pid)
709
  if isinstance(out, dict) and out.get("ok"):
@@ -715,20 +882,89 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
715
  await safe_reply(m, "Cancelled.", reply_markup=main_menu_keyboard())
716
  return
717
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
718
  # Edit title/desc
719
  if uid in _AWAIT_EDIT:
720
- st = _AWAIT_EDIT.pop(uid)
721
  p = _PENDING_UPLOAD.get(uid)
722
  if not p:
723
  return
724
- txt = (m.text or "").strip()
725
  lines = txt.splitlines()
726
  if len(lines) == 1:
727
- p.title = lines[0].strip()[:Settings.MAX_TITLE]
728
  else:
729
- p.title = lines[0].strip()[:Settings.MAX_TITLE]
730
- p.description = "\n".join(lines[1:]).strip()[:Settings.MAX_DESC]
 
731
  _PENDING_UPLOAD[uid] = p
732
- media, _, size = _media_and_filename(p.src_msg)
733
  await safe_reply(m, _render_preview(p, size), reply_markup=upload_confirm_keyboard(p.privacy))
734
  return
 
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
 
 
11
  from typing import Any, Dict, Optional, Tuple
12
 
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,
39
+ MENU_SPEED,
40
+ UP_DEL,
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
 
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
 
 
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
 
 
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
 
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()
 
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))
 
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
281
  ul_start = time.time()
 
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)
 
318
  if detail:
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
 
 
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
  _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)
 
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)
384
  _PENDING_DELETE.pop(uid, None)
385
 
 
386
  task = _UPLOAD_TASK.pop(uid, None)
387
  if task and not task.done():
388
  task.cancel()
 
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
  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
 
 
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
  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
 
 
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"
 
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)
 
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) ----
 
562
  if user_app is None:
563
  await safe_reply(m, "❌ This mode requires user session (user_app missing).")
564
  return
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:
582
  src = await user_app.get_messages(chat_ref, msg_id)
583
  except Exception as e:
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)
 
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)
 
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:
 
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
  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)
 
781
  cycle = {"private": "unlisted", "unlisted": "public", "public": "private"}
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
 
 
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
 
 
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
  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
  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
  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