understanding commited on
Commit
93a255c
·
verified ·
1 Parent(s): 12a3d95

Update bot/handlers.py

Browse files
Files changed (1) hide show
  1. bot/handlers.py +301 -65
bot/handlers.py CHANGED
@@ -8,6 +8,7 @@ import asyncio
8
  from dataclasses import dataclass
9
  from typing import Any, Dict, Optional, Tuple, Union
10
 
 
11
  from hydrogram import Client, filters
12
  from hydrogram.types import (
13
  Message,
@@ -16,17 +17,14 @@ from hydrogram.types import (
16
  InlineKeyboardButton,
17
  )
18
 
 
19
  from bot.core.auth import require_allowed, is_owner_id
20
  from bot.core.progress import SpeedETA, human_bytes, human_eta
21
 
22
- # ✅ profile_* are ADMIN routes => cf_worker1
23
- from bot.integrations.cf_worker1 import (
24
- profile_add,
25
- profile_list,
26
- profile_set_default,
27
- )
28
 
29
- # ✅ HF/private routes => cf_worker2
30
  from bot.integrations.cf_worker2 import (
31
  pick_profile,
32
  access_token,
@@ -44,7 +42,6 @@ from bot.temp.files import cleanup_file
44
  from bot.ui.keyboards import (
45
  main_menu_keyboard,
46
  auth_menu_keyboard,
47
- profiles_keyboard,
48
  upload_confirm_keyboard,
49
  )
50
  from bot.ui.callbacks import (
@@ -71,7 +68,6 @@ from bot.ui.texts import (
71
  NEED_AUTH,
72
  )
73
 
74
-
75
  # =========================
76
  # In-memory state (per-user)
77
  # =========================
@@ -94,6 +90,8 @@ _PENDING_UPLOAD: Dict[int, PendingUpload] = {} # uid -> pending upload
94
  _IN_PROGRESS: Dict[int, bool] = {} # uid -> upload is running
95
  _SPEED_COOLDOWN_UNTIL: Dict[int, float] = {} # uid -> unix time
96
 
 
 
97
 
98
  # =========================
99
  # Helpers
@@ -102,7 +100,7 @@ _SPEED_COOLDOWN_UNTIL: Dict[int, float] = {} # uid -> unix time
102
  def _admin_ids_from_env() -> set[int]:
103
  raw = (os.getenv("ADMIN_IDS") or "").strip()
104
  out: set[int] = set()
105
- for part in raw.split(","):
106
  part = part.strip()
107
  if not part:
108
  continue
@@ -166,17 +164,6 @@ def _render_preview(p: PendingUpload) -> str:
166
  )
167
 
168
 
169
- def _pick_login_url(resp: Any) -> Optional[str]:
170
- if not isinstance(resp, dict):
171
- return None
172
- if isinstance(resp.get("login_url"), str):
173
- return resp["login_url"]
174
- data = resp.get("data")
175
- if isinstance(data, dict) and isinstance(data.get("login_url"), str):
176
- return data["login_url"]
177
- return None
178
-
179
-
180
  def parse_telegram_link(link: str) -> Tuple[Union[int, str], int]:
181
  s = (link or "").strip()
182
  if not s:
@@ -215,12 +202,186 @@ async def _ensure_allowed(m: Message) -> bool:
215
  if not uid:
216
  await safe_reply(m, NOT_ALLOWED)
217
  return False
 
 
 
 
 
218
  if not await require_allowed(uid):
219
  await safe_reply(m, NOT_ALLOWED)
220
  return False
221
  return True
222
 
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  async def _start_pending_upload(
225
  *,
226
  chat_msg: Message,
@@ -271,7 +432,8 @@ async def _run_upload(uid: int) -> None:
271
  file_path = None
272
 
273
  try:
274
- pl = await profile_list(uid)
 
275
  if not (isinstance(pl, dict) and pl.get("ok")):
276
  await safe_edit(st, "❌ Failed to fetch profiles. Try /profiles again.")
277
  return
@@ -281,11 +443,24 @@ async def _run_upload(uid: int) -> None:
281
  await safe_edit(st, NEED_AUTH)
282
  return
283
 
284
- default_id = pl.get("default_profile_id") or profiles[0].get("profile_id")
285
- default_profile = next((p for p in profiles if p.get("profile_id") == default_id), profiles[0])
286
- channel_id = default_profile.get("channel_id")
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  if not channel_id:
288
- await safe_edit(st, "❌ Default profile is not connected. Use /profiles set a connected one as default.")
289
  return
290
 
291
  pick = await pick_profile(uid, channel_id)
@@ -387,8 +562,26 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
387
  _AWAIT_AUTH_MODE.pop(uid, None)
388
  _AWAIT_EDIT.pop(uid, None)
389
  _PENDING_UPLOAD.pop(uid, None)
 
390
  await safe_reply(m, CANCELLED, reply_markup=main_menu_keyboard())
391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  # owner/admin
393
  @app.on_message(filters.command("allow") & filters.private)
394
  async def allow_cmd(_, m: Message) -> None:
@@ -437,16 +630,7 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
437
  if not await _ensure_allowed(m):
438
  return
439
  uid = m.from_user.id
440
- pl = await profile_list(uid)
441
- if not (isinstance(pl, dict) and pl.get("ok")):
442
- err = pl.get("err", "unknown") if isinstance(pl, dict) else "unknown"
443
- await safe_reply(m, f"❌ profile_list failed: `{err}`")
444
- return
445
- await safe_reply(
446
- m,
447
- f"Profiles for `{uid}`\nDefault: `{pl.get('default_profile_id')}`\n\nPick default:",
448
- reply_markup=profiles_keyboard(pl),
449
- )
450
 
451
  # incoming media
452
  @app.on_message((filters.video | filters.document) & filters.private)
@@ -520,25 +704,17 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
520
  return
521
 
522
  if action == MENU_AUTH:
523
- if not await require_allowed(uid):
524
  await safe_edit(q.message, NOT_ALLOWED, reply_markup=main_menu_keyboard())
525
  return
526
  await safe_edit(q.message, "Choose auth mode:", reply_markup=auth_menu_keyboard())
527
  return
528
 
529
  if action == MENU_PROFILES:
530
- if not await require_allowed(uid):
531
  await safe_edit(q.message, NOT_ALLOWED, reply_markup=main_menu_keyboard())
532
  return
533
- pl = await profile_list(uid)
534
- if not (isinstance(pl, dict) and pl.get("ok")):
535
- await safe_edit(q.message, "❌ Failed to load profiles.", reply_markup=main_menu_keyboard())
536
- return
537
- await safe_edit(
538
- q.message,
539
- f"Profiles for `{uid}`\nDefault: `{pl.get('default_profile_id')}`\n\nPick default:",
540
- reply_markup=profiles_keyboard(pl),
541
- )
542
  return
543
 
544
  if action == MENU_SPEEDTEST:
@@ -575,6 +751,79 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
575
  await safe_edit(q.message, txt, reply_markup=main_menu_keyboard())
576
  return
577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
  if action == AUTH_JSON:
579
  _AWAIT_AUTH_MODE[uid] = "json"
580
  await safe_edit(
@@ -593,21 +842,7 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
593
  )
594
  return
595
 
596
- if action == "setdef":
597
- if not await require_allowed(uid):
598
- await safe_edit(q.message, NOT_ALLOWED, reply_markup=main_menu_keyboard())
599
- return
600
- prof_id = (value or "").strip()
601
- if not prof_id:
602
- return
603
- r = await profile_set_default(uid, prof_id)
604
- if isinstance(r, dict) and r.get("ok"):
605
- await safe_edit(q.message, "✅ Default profile updated.", reply_markup=main_menu_keyboard())
606
- else:
607
- err = r.get("err", "unknown") if isinstance(r, dict) else "unknown"
608
- await safe_edit(q.message, f"❌ Failed: `{err}`", reply_markup=main_menu_keyboard())
609
- return
610
-
611
  if action in (UP_GO, UP_EDIT, UP_PRIV, UP_CANCEL):
612
  pending = _PENDING_UPLOAD.get(uid)
613
  if not pending:
@@ -679,13 +914,14 @@ def setup_handlers(app: Client, user_app: Optional[Client] = None) -> None:
679
  await safe_reply(m, f"❌ profile_add failed: `{err}`")
680
  return
681
 
682
- login_url = _pick_login_url(resp)
683
  if not login_url:
684
- await safe_reply(m, "✅ Profile added, but login link missing from response. Check your Pages backend.")
 
685
  return
686
 
687
  await safe_reply(m, f"✅ Profile added.\n\nOpen this link to authorize:\n{login_url}")
688
- await safe_reply(m, "After authorizing, send a video/document here to upload.", reply_markup=main_menu_keyboard())
689
 
690
  except Exception as e:
691
  await safe_reply(m, f"❌ Parse error: `{str(e)[:180]}`\n\nTry /auth again.")
 
8
  from dataclasses import dataclass
9
  from typing import Any, Dict, Optional, Tuple, Union
10
 
11
+ import httpx
12
  from hydrogram import Client, filters
13
  from hydrogram.types import (
14
  Message,
 
17
  InlineKeyboardButton,
18
  )
19
 
20
+ from bot.config import Workers
21
  from bot.core.auth import require_allowed, is_owner_id
22
  from bot.core.progress import SpeedETA, human_bytes, human_eta
23
 
24
+ # ✅ profile_add is ADMIN route => cf_worker1 (Pages, BOT_BACKEND_KEY)
25
+ from bot.integrations.cf_worker1 import profile_add
 
 
 
 
26
 
27
+ # ✅ HF/private routes => cf_worker2 (Pages, HF_API_KEY)
28
  from bot.integrations.cf_worker2 import (
29
  pick_profile,
30
  access_token,
 
42
  from bot.ui.keyboards import (
43
  main_menu_keyboard,
44
  auth_menu_keyboard,
 
45
  upload_confirm_keyboard,
46
  )
47
  from bot.ui.callbacks import (
 
68
  NEED_AUTH,
69
  )
70
 
 
71
  # =========================
72
  # In-memory state (per-user)
73
  # =========================
 
90
  _IN_PROGRESS: Dict[int, bool] = {} # uid -> upload is running
91
  _SPEED_COOLDOWN_UNTIL: Dict[int, float] = {} # uid -> unix time
92
 
93
+ _PENDING_DELETE: Dict[int, str] = {} # uid -> profile_id pending delete confirmation
94
+
95
 
96
  # =========================
97
  # Helpers
 
100
  def _admin_ids_from_env() -> set[int]:
101
  raw = (os.getenv("ADMIN_IDS") or "").strip()
102
  out: set[int] = set()
103
+ for part in raw.replace(";", ",").replace("|", ",").split(","):
104
  part = part.strip()
105
  if not part:
106
  continue
 
164
  )
165
 
166
 
 
 
 
 
 
 
 
 
 
 
 
167
  def parse_telegram_link(link: str) -> Tuple[Union[int, str], int]:
168
  s = (link or "").strip()
169
  if not s:
 
202
  if not uid:
203
  await safe_reply(m, NOT_ALLOWED)
204
  return False
205
+
206
+ # ✅ owner/admin must never get blocked by allowlist
207
+ if _is_admin_or_owner(uid):
208
+ return True
209
+
210
  if not await require_allowed(uid):
211
  await safe_reply(m, NOT_ALLOWED)
212
  return False
213
  return True
214
 
215
 
216
+ # =========================
217
+ # Worker calls (no integration dependency)
218
+ # =========================
219
+
220
+ _HTTP_TIMEOUT = httpx.Timeout(20.0, connect=10.0)
221
+
222
+ async def _post_json(url: str, bearer: str, payload: dict) -> dict:
223
+ headers = {"authorization": f"Bearer {bearer}"}
224
+ try:
225
+ async with httpx.AsyncClient(timeout=_HTTP_TIMEOUT) as c:
226
+ r = await c.post(url, headers=headers, json=payload)
227
+ ct = (r.headers.get("content-type") or "").lower()
228
+ if "application/json" in ct:
229
+ j = r.json()
230
+ return j if isinstance(j, dict) else {"ok": False, "err": "bad_json"}
231
+ # Pages sometimes returns text (Unauthorized etc)
232
+ return {"ok": False, "err": f"http_{r.status_code}", "detail": (r.text or "")[:200]}
233
+ except Exception as e:
234
+ return {"ok": False, "err": "network_error", "detail": str(e)[:200]}
235
+
236
+
237
+ async def _hf_list_profiles(uid: int, only_connected: bool = False) -> dict:
238
+ if not Workers.WORKER2_URL or not Workers.HF_API_KEY:
239
+ return {"ok": False, "err": "WORKER2_URL/HF_API_KEY missing"}
240
+ return await _post_json(
241
+ f"{Workers.WORKER2_URL}/api/list_profiles",
242
+ Workers.HF_API_KEY,
243
+ {"tg_id": str(uid), "only_connected": bool(only_connected)},
244
+ )
245
+
246
+
247
+ async def _admin_profile_set_default(uid: int, profile_id: str) -> dict:
248
+ if not Workers.WORKER1_URL or not Workers.BOT_BACKEND_KEY:
249
+ return {"ok": False, "err": "WORKER1_URL/BOT_BACKEND_KEY missing"}
250
+ return await _post_json(
251
+ f"{Workers.WORKER1_URL}/api/profile/set_default",
252
+ Workers.BOT_BACKEND_KEY,
253
+ {"tg_id": str(uid), "profile_id": str(profile_id)},
254
+ )
255
+
256
+
257
+ async def _admin_profile_remove(uid: int, profile_id: str) -> dict:
258
+ if not Workers.WORKER1_URL or not Workers.BOT_BACKEND_KEY:
259
+ return {"ok": False, "err": "WORKER1_URL/BOT_BACKEND_KEY missing"}
260
+ return await _post_json(
261
+ f"{Workers.WORKER1_URL}/api/profile/remove",
262
+ Workers.BOT_BACKEND_KEY,
263
+ {"tg_id": str(uid), "profile_id": str(profile_id)},
264
+ )
265
+
266
+
267
+ async def _admin_profile_login_link(uid: int, profile_id: str, force: bool = False) -> dict:
268
+ if not Workers.WORKER1_URL or not Workers.BOT_BACKEND_KEY:
269
+ return {"ok": False, "err": "WORKER1_URL/BOT_BACKEND_KEY missing"}
270
+ return await _post_json(
271
+ f"{Workers.WORKER1_URL}/api/profile/login_link",
272
+ Workers.BOT_BACKEND_KEY,
273
+ {"tg_id": str(uid), "profile_id": str(profile_id), "ttl_sec": 600, "force": bool(force)},
274
+ )
275
+
276
+
277
+ # =========================
278
+ # Profiles UI (fixed)
279
+ # =========================
280
+
281
+ def _is_connected_profile(p: dict) -> bool:
282
+ return bool(p.get("has_refresh")) and bool(p.get("channel_id"))
283
+
284
+
285
+ def _profiles_text(uid: int, pl: dict) -> str:
286
+ default_id = pl.get("default_profile_id") or "—"
287
+ day = pl.get("day") or ""
288
+ profiles = pl.get("profiles") or []
289
+ if not profiles:
290
+ return (
291
+ f"👤 Profiles for `{uid}`\n\n"
292
+ "No profiles found.\n\n"
293
+ "Use **Add Profile** (or /auth) to add credentials."
294
+ )
295
+
296
+ lines = [
297
+ f"👤 Profiles for `{uid}`",
298
+ f"Default: `{default_id}`",
299
+ ]
300
+ if day:
301
+ lines.append(f"Day: `{day}`")
302
+ lines.append("")
303
+ lines.append("Legend: ✅ connected | ⚠️ not connected")
304
+ lines.append("")
305
+
306
+ for p in profiles:
307
+ pid = p.get("profile_id") or ""
308
+ label = (p.get("label") or pid[:6] or "profile").strip()
309
+ mark = "⭐" if pid == default_id else " "
310
+ conn = "✅" if _is_connected_profile(p) else "⚠️"
311
+ ch = p.get("channel_title") or p.get("channel_id") or "—"
312
+ used = p.get("used_today")
313
+ used_txt = f" • used:{used}" if isinstance(used, int) else ""
314
+ lines.append(f"{mark}{conn} **{label}** — `{pid}` — {ch}{used_txt}")
315
+
316
+ lines.append("")
317
+ lines.append("Tap buttons below to set default / login / delete.")
318
+ return "\n".join(lines)
319
+
320
+
321
+ def _profiles_markup(pl: dict) -> InlineKeyboardMarkup:
322
+ profiles = pl.get("profiles") or []
323
+ default_id = pl.get("default_profile_id") or ""
324
+
325
+ rows: list[list[InlineKeyboardButton]] = []
326
+
327
+ for p in profiles:
328
+ pid = str(p.get("profile_id") or "")
329
+ if not pid:
330
+ continue
331
+ connected = _is_connected_profile(p)
332
+ is_def = (pid == default_id)
333
+
334
+ # Row: Default + Login/Reauth + Delete
335
+ btn_default = InlineKeyboardButton(
336
+ "✅ Default" if is_def else "⭐ Set default",
337
+ callback_data=make("pdef", pid) if not is_def else make("pref"),
338
+ )
339
+ btn_login = InlineKeyboardButton(
340
+ "🔁 Re-auth" if connected else "🔑 Login",
341
+ callback_data=make("preauth" if connected else "plog", pid),
342
+ )
343
+ btn_del = InlineKeyboardButton("🗑 Delete", callback_data=make("pdel", pid))
344
+
345
+ rows.append([btn_default, btn_login, btn_del])
346
+
347
+ # Footer
348
+ rows.append(
349
+ [
350
+ InlineKeyboardButton("🔄 Refresh", callback_data=make("pref")),
351
+ InlineKeyboardButton("➕ Add profile", callback_data=make(MENU_AUTH)),
352
+ ]
353
+ )
354
+ rows.append([InlineKeyboardButton("⬅️ Back", callback_data=make(BACK))])
355
+
356
+ return InlineKeyboardMarkup(rows)
357
+
358
+
359
+ async def _show_profiles(msg: Message, uid: int, *, edit: bool) -> None:
360
+ pl = await _hf_list_profiles(uid, only_connected=False)
361
+ if not (isinstance(pl, dict) and pl.get("ok")):
362
+ err = pl.get("err", "unknown") if isinstance(pl, dict) else "unknown"
363
+ detail = pl.get("detail") if isinstance(pl, dict) else None
364
+ txt = f"❌ Failed to load profiles: `{err}`"
365
+ if detail:
366
+ txt += f"\n`{str(detail)[:180]}`"
367
+ if edit:
368
+ await safe_edit(msg, txt, reply_markup=main_menu_keyboard())
369
+ else:
370
+ await safe_reply(msg, txt, reply_markup=main_menu_keyboard())
371
+ return
372
+
373
+ txt = _profiles_text(uid, pl)
374
+ kb = _profiles_markup(pl)
375
+ if edit:
376
+ await safe_edit(msg, txt, reply_markup=kb)
377
+ else:
378
+ await safe_reply(msg, txt, reply_markup=kb)
379
+
380
+
381
+ # =========================
382
+ # Upload flow
383
+ # =========================
384
+
385
  async def _start_pending_upload(
386
  *,
387
  chat_msg: Message,
 
432
  file_path = None
433
 
434
  try:
435
+ # Use HF list (shows default + connection state)
436
+ pl = await _hf_list_profiles(uid, only_connected=False)
437
  if not (isinstance(pl, dict) and pl.get("ok")):
438
  await safe_edit(st, "❌ Failed to fetch profiles. Try /profiles again.")
439
  return
 
443
  await safe_edit(st, NEED_AUTH)
444
  return
445
 
446
+ default_id = pl.get("default_profile_id") or ""
447
+ default_profile = next((p for p in profiles if p.get("profile_id") == default_id), None)
448
+
449
+ # Default must be connected; otherwise fallback to first connected and instruct user
450
+ chosen = default_profile if default_profile and _is_connected_profile(default_profile) else None
451
+ if not chosen:
452
+ chosen = next((p for p in profiles if _is_connected_profile(p)), None)
453
+
454
+ if not chosen:
455
+ await safe_edit(
456
+ st,
457
+ "❌ No connected YouTube profile.\n\nOpen /profiles → tap **Login** to authorize a profile.",
458
+ )
459
+ return
460
+
461
+ channel_id = chosen.get("channel_id")
462
  if not channel_id:
463
+ await safe_edit(st, "❌ चुने हुए profile में channel_id missing है. /profiles में Re-auth करो.")
464
  return
465
 
466
  pick = await pick_profile(uid, channel_id)
 
562
  _AWAIT_AUTH_MODE.pop(uid, None)
563
  _AWAIT_EDIT.pop(uid, None)
564
  _PENDING_UPLOAD.pop(uid, None)
565
+ _PENDING_DELETE.pop(uid, None)
566
  await safe_reply(m, CANCELLED, reply_markup=main_menu_keyboard())
567
 
568
+ # ✅ diag (owner/admin) - fixes “command not working” perception
569
+ @app.on_message(filters.command("diag") & filters.private)
570
+ async def diag_cmd(_, m: Message) -> None:
571
+ uid = m.from_user.id
572
+ if not _is_admin_or_owner(uid):
573
+ await safe_reply(m, OWNER_ONLY)
574
+ return
575
+ hf = await _hf_list_profiles(uid, only_connected=False)
576
+ txt = {
577
+ "ok": True,
578
+ "worker1_url": Workers.WORKER1_URL,
579
+ "worker2_url": Workers.WORKER2_URL,
580
+ "hf_list_profiles_ok": bool(isinstance(hf, dict) and hf.get("ok")),
581
+ "hf_err": hf.get("err") if isinstance(hf, dict) else "bad_resp",
582
+ }
583
+ await safe_reply(m, json.dumps(txt, indent=2)[:3900], reply_markup=main_menu_keyboard())
584
+
585
  # owner/admin
586
  @app.on_message(filters.command("allow") & filters.private)
587
  async def allow_cmd(_, m: Message) -> None:
 
630
  if not await _ensure_allowed(m):
631
  return
632
  uid = m.from_user.id
633
+ await _show_profiles(m, uid, edit=False)
 
 
 
 
 
 
 
 
 
634
 
635
  # incoming media
636
  @app.on_message((filters.video | filters.document) & filters.private)
 
704
  return
705
 
706
  if action == MENU_AUTH:
707
+ if not await require_allowed(uid) and not _is_admin_or_owner(uid):
708
  await safe_edit(q.message, NOT_ALLOWED, reply_markup=main_menu_keyboard())
709
  return
710
  await safe_edit(q.message, "Choose auth mode:", reply_markup=auth_menu_keyboard())
711
  return
712
 
713
  if action == MENU_PROFILES:
714
+ if not await require_allowed(uid) and not _is_admin_or_owner(uid):
715
  await safe_edit(q.message, NOT_ALLOWED, reply_markup=main_menu_keyboard())
716
  return
717
+ await _show_profiles(q.message, uid, edit=True)
 
 
 
 
 
 
 
 
718
  return
719
 
720
  if action == MENU_SPEEDTEST:
 
751
  await safe_edit(q.message, txt, reply_markup=main_menu_keyboard())
752
  return
753
 
754
+ # ===== Profiles actions (fixed) =====
755
+ if action == "pref":
756
+ await _show_profiles(q.message, uid, edit=True)
757
+ return
758
+
759
+ if action == "pdef":
760
+ pid = (value or "").strip()
761
+ if not pid:
762
+ return
763
+ r = await _admin_profile_set_default(uid, pid)
764
+ if isinstance(r, dict) and r.get("ok"):
765
+ await _show_profiles(q.message, uid, edit=True)
766
+ else:
767
+ err = r.get("err", "unknown") if isinstance(r, dict) else "unknown"
768
+ await safe_edit(q.message, f"❌ Set default failed: `{err}`", reply_markup=main_menu_keyboard())
769
+ return
770
+
771
+ if action == "pdel":
772
+ pid = (value or "").strip()
773
+ if not pid:
774
+ return
775
+ _PENDING_DELETE[uid] = pid
776
+ kb = InlineKeyboardMarkup(
777
+ [
778
+ [
779
+ InlineKeyboardButton("✅ Yes, delete", callback_data=make("pdelok", pid)),
780
+ InlineKeyboardButton("❌ Cancel", callback_data=make("pref")),
781
+ ]
782
+ ]
783
+ )
784
+ await safe_edit(q.message, f"⚠️ Delete profile `{pid}` ?", reply_markup=kb)
785
+ return
786
+
787
+ if action == "pdelok":
788
+ pid = (value or "").strip()
789
+ if not pid:
790
+ return
791
+ if _PENDING_DELETE.get(uid) != pid:
792
+ await _show_profiles(q.message, uid, edit=True)
793
+ return
794
+ _PENDING_DELETE.pop(uid, None)
795
+ r = await _admin_profile_remove(uid, pid)
796
+ if isinstance(r, dict) and r.get("ok"):
797
+ await _show_profiles(q.message, uid, edit=True)
798
+ else:
799
+ err = r.get("err", "unknown") if isinstance(r, dict) else "unknown"
800
+ await safe_edit(q.message, f"❌ Delete failed: `{err}`", reply_markup=main_menu_keyboard())
801
+ return
802
+
803
+ if action in ("plog", "preauth"):
804
+ pid = (value or "").strip()
805
+ if not pid:
806
+ return
807
+ force = (action == "preauth")
808
+ r = await _admin_profile_login_link(uid, pid, force=force)
809
+ if not (isinstance(r, dict) and r.get("ok") and r.get("login_url")):
810
+ err = r.get("err", "unknown") if isinstance(r, dict) else "unknown"
811
+ await safe_edit(q.message, f"❌ Login link failed: `{err}`", reply_markup=main_menu_keyboard())
812
+ return
813
+ url = str(r.get("login_url"))
814
+ kb = InlineKeyboardMarkup(
815
+ [
816
+ [InlineKeyboardButton("🌐 Open Google Login", url=url)],
817
+ [InlineKeyboardButton("🔄 Back to profiles", callback_data=make(MENU_PROFILES))],
818
+ ]
819
+ )
820
+ msg = "🔑 Open this link to authorize your YouTube channel."
821
+ if force:
822
+ msg = "🔁 Re-auth link generated (fresh consent). Open this link:"
823
+ await safe_edit(q.message, msg, reply_markup=kb)
824
+ return
825
+
826
+ # ===== Auth mode selection =====
827
  if action == AUTH_JSON:
828
  _AWAIT_AUTH_MODE[uid] = "json"
829
  await safe_edit(
 
842
  )
843
  return
844
 
845
+ # ===== Upload controls =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  if action in (UP_GO, UP_EDIT, UP_PRIV, UP_CANCEL):
847
  pending = _PENDING_UPLOAD.get(uid)
848
  if not pending:
 
914
  await safe_reply(m, f"❌ profile_add failed: `{err}`")
915
  return
916
 
917
+ login_url = resp.get("login_url")
918
  if not login_url:
919
+ # fallback: tell user to open Profiles and tap Login
920
+ await safe_reply(m, "✅ Profile added. Now open /profiles and tap **Login** to authorize.")
921
  return
922
 
923
  await safe_reply(m, f"✅ Profile added.\n\nOpen this link to authorize:\n{login_url}")
924
+ await safe_reply(m, "After authorizing, open /profiles and set default.", reply_markup=main_menu_keyboard())
925
 
926
  except Exception as e:
927
  await safe_reply(m, f"❌ Parse error: `{str(e)[:180]}`\n\nTry /auth again.")