Spaces:
Runtime error
Runtime error
Update bot/handlers.py
Browse files- bot/handlers.py +341 -62
bot/handlers.py
CHANGED
|
@@ -2,48 +2,65 @@
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import asyncio
|
|
|
|
| 5 |
import os
|
| 6 |
import re
|
|
|
|
| 7 |
import time
|
| 8 |
from dataclasses import dataclass
|
| 9 |
-
from typing import Any, Dict, Optional, Tuple,
|
| 10 |
|
|
|
|
| 11 |
from hydrogram import Client, filters
|
| 12 |
from hydrogram.types import CallbackQuery, Message
|
| 13 |
|
| 14 |
-
from bot.config import Auth, Workers
|
|
|
|
|
|
|
|
|
|
| 15 |
from bot.integrations.auth import allow_user, disallow_user, get_stats, is_allowed
|
| 16 |
-
from bot.integrations.cf_worker1 import profile_check_auth, profile_delete
|
| 17 |
-
from bot.integrations.cf_worker2 import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from bot.telegram.files import cleanup_file
|
| 19 |
from bot.telegram.media import download_to_temp
|
| 20 |
from bot.telegram.replies import safe_edit, safe_reply
|
| 21 |
from bot.ui.callbacks import (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
MENU_AUTH,
|
| 23 |
MENU_HELP,
|
| 24 |
MENU_PROFILES,
|
| 25 |
MENU_SPEED,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
UP_DEL,
|
| 27 |
UP_EDIT,
|
| 28 |
UP_GO,
|
| 29 |
UP_PRIV,
|
| 30 |
-
UP_CANCEL,
|
| 31 |
-
NAME_ORIGINAL,
|
| 32 |
-
NAME_CAPTION,
|
| 33 |
-
NAME_CUSTOM,
|
| 34 |
-
BACK,
|
| 35 |
)
|
| 36 |
-
from bot.ui.keyboards import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
from bot.ui.parse import parse_cb
|
| 38 |
-
from bot.ui.texts import CANCELLED, HELP_TEXT, NEED_AUTH, NOT_ALLOWED, OWNER_ONLY
|
| 39 |
-
from bot.core.progress import SpeedETA, human_bytes, human_eta
|
| 40 |
-
from bot.core.settings import Settings
|
| 41 |
-
from bot.core.uptime import uptime_text
|
| 42 |
from bot.youtube.link_parser import parse_telegram_link
|
| 43 |
|
| 44 |
ChatRef = Union[int, str]
|
| 45 |
|
| 46 |
-
|
| 47 |
@dataclass
|
| 48 |
class PendingUpload:
|
| 49 |
request_msg: Message # where user triggered command / sent media
|
|
@@ -74,7 +91,6 @@ class EditState:
|
|
| 74 |
privacy: str
|
| 75 |
|
| 76 |
|
| 77 |
-
# In-memory state
|
| 78 |
_PENDING_UPLOAD: Dict[int, PendingUpload] = {}
|
| 79 |
_AWAIT_EDIT: Dict[int, EditState] = {}
|
| 80 |
_PENDING_DELETE: Dict[int, str] = {}
|
|
@@ -82,9 +98,36 @@ _IN_PROGRESS: Dict[int, bool] = {}
|
|
| 82 |
_UPLOAD_TASK: Dict[int, asyncio.Task] = {}
|
| 83 |
_BATCH_TASK: Dict[int, asyncio.Task] = {}
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
def _is_admin_or_owner(uid: int) -> bool:
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
|
| 90 |
async def _ensure_allowed_uid(uid: int, reply_target: Message) -> bool:
|
|
@@ -106,9 +149,9 @@ async def _ensure_allowed_uid(uid: int, reply_target: Message) -> bool:
|
|
| 106 |
|
| 107 |
def _media_and_filename(m: Message) -> Tuple[Optional[Any], str, int]:
|
| 108 |
if m.video:
|
| 109 |
-
return m.video, (m.video.file_name or "video.mp4"), (m.video.file_size or 0)
|
| 110 |
if m.document:
|
| 111 |
-
return m.document, (m.document.file_name or "file.bin"), (m.document.file_size or 0)
|
| 112 |
return None, "", 0
|
| 113 |
|
| 114 |
|
|
@@ -246,6 +289,20 @@ def _as_int(v: Any) -> int:
|
|
| 246 |
return 0
|
| 247 |
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
async def _run_upload(uid: int) -> Dict[str, Any]:
|
| 250 |
"""
|
| 251 |
Runs one upload for uid, editing p.status_msg.
|
|
@@ -392,6 +449,7 @@ async def _run_upload(uid: int) -> Dict[str, Any]:
|
|
| 392 |
_PENDING_UPLOAD.pop(uid, None)
|
| 393 |
_AWAIT_EDIT.pop(uid, None)
|
| 394 |
_PENDING_DELETE.pop(uid, None)
|
|
|
|
| 395 |
if file_path:
|
| 396 |
try:
|
| 397 |
cleanup_file(file_path)
|
|
@@ -416,7 +474,6 @@ def _parse_link_or_range(line: str) -> Tuple[ChatRef, int, int]:
|
|
| 416 |
if not s:
|
| 417 |
raise ValueError("empty")
|
| 418 |
|
| 419 |
-
# quick detect ".... / 4012-4046"
|
| 420 |
m = _BATCH_RANGE_RE.match(s)
|
| 421 |
if m:
|
| 422 |
base = m.group(1).strip()
|
|
@@ -427,25 +484,173 @@ def _parse_link_or_range(line: str) -> Tuple[ChatRef, int, int]:
|
|
| 427 |
if a > b:
|
| 428 |
a, b = b, a
|
| 429 |
|
| 430 |
-
# rebuild a normal single link to extract chat_ref
|
| 431 |
single = f"{base}/{a}"
|
| 432 |
chat_ref, _mid = parse_telegram_link(single)
|
| 433 |
return chat_ref, a, b
|
| 434 |
|
| 435 |
-
# normal single link
|
| 436 |
chat_ref, mid = parse_telegram_link(s)
|
| 437 |
return chat_ref, int(mid), int(mid)
|
| 438 |
|
| 439 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
| 441 |
# ---- basic ----
|
| 442 |
@app.on_message(filters.command(["start", "help"]))
|
| 443 |
async def start_help_cmd(_, m: Message) -> None:
|
| 444 |
-
# always show menu + help text
|
| 445 |
if (m.text or "").strip().lower().startswith("/help"):
|
| 446 |
await safe_reply(m, HELP_TEXT, reply_markup=main_menu_keyboard())
|
| 447 |
else:
|
| 448 |
-
from bot.ui.texts import START_TEXT
|
| 449 |
await safe_reply(m, START_TEXT, reply_markup=main_menu_keyboard())
|
| 450 |
|
| 451 |
@app.on_message(filters.command("speedtest"))
|
|
@@ -453,11 +658,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 453 |
uid = m.from_user.id if m.from_user else 0
|
| 454 |
if not await _ensure_allowed_uid(uid, m):
|
| 455 |
return
|
| 456 |
-
from bot.core.speedtest import net_download_test, net_upload_test
|
| 457 |
-
|
|
|
|
| 458 |
dl = await net_download_test()
|
| 459 |
ul = await net_upload_test()
|
| 460 |
-
ip = await
|
| 461 |
|
| 462 |
dl_bps = float((dl or {}).get("bps", 0) or 0)
|
| 463 |
ul_bps = float((ul or {}).get("bps", 0) or 0)
|
|
@@ -469,11 +675,16 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 469 |
f"*Download:* `{human_bytes(dl_bps)}/s`\n"
|
| 470 |
f"*Upload:* `{human_bytes(ul_bps)}/s`"
|
| 471 |
)
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
@app.on_message(filters.command("cancel"))
|
| 475 |
async def cancel_cmd(_, m: Message) -> None:
|
| 476 |
-
uid = m.from_user.id
|
|
|
|
|
|
|
| 477 |
_AWAIT_EDIT.pop(uid, None)
|
| 478 |
_PENDING_UPLOAD.pop(uid, None)
|
| 479 |
_PENDING_DELETE.pop(uid, None)
|
|
@@ -493,7 +704,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 493 |
# ---- admin allowlist ----
|
| 494 |
@app.on_message(filters.command("allow"))
|
| 495 |
async def allow_cmd(_, m: Message) -> None:
|
| 496 |
-
uid = m.from_user.id
|
| 497 |
if not _is_admin_or_owner(uid):
|
| 498 |
await safe_reply(m, OWNER_ONLY)
|
| 499 |
return
|
|
@@ -511,7 +722,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 511 |
|
| 512 |
@app.on_message(filters.command("disallow"))
|
| 513 |
async def disallow_cmd(_, m: Message) -> None:
|
| 514 |
-
uid = m.from_user.id
|
| 515 |
if not _is_admin_or_owner(uid):
|
| 516 |
await safe_reply(m, OWNER_ONLY)
|
| 517 |
return
|
|
@@ -529,7 +740,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 529 |
|
| 530 |
@app.on_message(filters.command("stats"))
|
| 531 |
async def stats_cmd(_, m: Message) -> None:
|
| 532 |
-
uid = m.from_user.id
|
| 533 |
if not _is_admin_or_owner(uid):
|
| 534 |
await safe_reply(m, OWNER_ONLY)
|
| 535 |
return
|
|
@@ -547,7 +758,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 547 |
|
| 548 |
@app.on_message(filters.command("diag"))
|
| 549 |
async def diag_cmd(_, m: Message) -> None:
|
| 550 |
-
uid = m.from_user.id
|
| 551 |
if not _is_admin_or_owner(uid):
|
| 552 |
await safe_reply(m, OWNER_ONLY)
|
| 553 |
return
|
|
@@ -561,20 +772,22 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 561 |
f"Uptime: `{uptime_text()}`\n"
|
| 562 |
f"WORKER1_URL: `{w1}`\n"
|
| 563 |
f"WORKER2_URL: `{w2}`\n"
|
| 564 |
-
f"Owners: `{len(Auth
|
|
|
|
| 565 |
)
|
| 566 |
|
| 567 |
# ---- auth/profile flow ----
|
| 568 |
@app.on_message(filters.command("auth"))
|
| 569 |
async def auth_cmd(_, m: Message) -> None:
|
| 570 |
-
uid = m.from_user.id
|
| 571 |
if not await _ensure_allowed_uid(uid, m):
|
| 572 |
return
|
|
|
|
| 573 |
await safe_reply(m, "π Add a profile:", reply_markup=auth_menu_keyboard())
|
| 574 |
|
| 575 |
@app.on_message(filters.command("profiles"))
|
| 576 |
async def profiles_cmd(_, m: Message) -> None:
|
| 577 |
-
uid = m.from_user.id
|
| 578 |
if not await _ensure_allowed_uid(uid, m):
|
| 579 |
return
|
| 580 |
data = await list_profiles(uid, only_connected=False)
|
|
@@ -585,10 +798,17 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 585 |
default_id = data.get("default_profile_id") or ""
|
| 586 |
await safe_reply(m, "π€ *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
|
| 587 |
|
| 588 |
-
# ---- upload from DM media ----
|
| 589 |
@app.on_message(filters.private & (filters.video | filters.document))
|
| 590 |
async def media_in_dm(_, m: Message) -> None:
|
|
|
|
|
|
|
| 591 |
uid = m.from_user.id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 592 |
if not await _ensure_allowed_uid(uid, m):
|
| 593 |
return
|
| 594 |
if _IN_PROGRESS.get(uid):
|
|
@@ -599,6 +819,8 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 599 |
# ---- upload from link (admin/owner) ----
|
| 600 |
@app.on_message(filters.command(["yt", "dl", "archive"]))
|
| 601 |
async def archive_cmd(_, m: Message) -> None:
|
|
|
|
|
|
|
| 602 |
uid = m.from_user.id
|
| 603 |
if not await _ensure_allowed_uid(uid, m):
|
| 604 |
return
|
|
@@ -636,6 +858,8 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 636 |
# ---- batch mode ----
|
| 637 |
@app.on_message(filters.command("batch"))
|
| 638 |
async def batch_cmd(_, m: Message) -> None:
|
|
|
|
|
|
|
| 639 |
uid = m.from_user.id
|
| 640 |
if not await _ensure_allowed_uid(uid, m):
|
| 641 |
return
|
|
@@ -665,7 +889,6 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 665 |
await safe_reply(m, "No links found. Put one t.me link per line after /batch")
|
| 666 |
return
|
| 667 |
|
| 668 |
-
# Expand links/ranges
|
| 669 |
items: List[Tuple[ChatRef, int]] = []
|
| 670 |
for ln in lines:
|
| 671 |
try:
|
|
@@ -677,8 +900,8 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 677 |
continue
|
| 678 |
|
| 679 |
count = (b - a + 1)
|
| 680 |
-
if count >
|
| 681 |
-
await safe_reply(m, f"β Range too large ({count}). Max is {
|
| 682 |
if not continue_on_fail:
|
| 683 |
return
|
| 684 |
continue
|
|
@@ -690,16 +913,16 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 690 |
await safe_reply(m, "No valid items to process.")
|
| 691 |
return
|
| 692 |
|
| 693 |
-
await safe_reply(
|
|
|
|
|
|
|
|
|
|
| 694 |
|
| 695 |
async def runner() -> None:
|
| 696 |
batch_start = time.time()
|
| 697 |
total = len(items)
|
| 698 |
-
for i, (chat_ref, msg_id) in enumerate(items, 1):
|
| 699 |
-
if _IN_PROGRESS.get(uid):
|
| 700 |
-
# should not happen (we do sequential), but keep safe
|
| 701 |
-
await asyncio.sleep(0.2)
|
| 702 |
|
|
|
|
| 703 |
st = await safe_reply(m, f"π Batch {i}/{total}: fetching message `{msg_id}`β¦")
|
| 704 |
if not st:
|
| 705 |
if not continue_on_fail:
|
|
@@ -714,7 +937,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 714 |
break
|
| 715 |
continue
|
| 716 |
|
| 717 |
-
media,
|
| 718 |
if not media:
|
| 719 |
await safe_edit(st, f"β Batch {i}/{total}: no media in that message. Skipped.")
|
| 720 |
continue
|
|
@@ -722,27 +945,25 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 722 |
# Create pending upload targeting THIS chat (m), not source chat
|
| 723 |
await _start_pending_upload(uid=uid, request_msg=m, src_msg=src, downloader=user_app, via_link=True)
|
| 724 |
|
| 725 |
-
# Force
|
| 726 |
p = _PENDING_UPLOAD.get(uid)
|
| 727 |
if p:
|
| 728 |
p.status_msg = st
|
| 729 |
_PENDING_UPLOAD[uid] = p
|
| 730 |
|
| 731 |
-
# Apply the current chosen title/desc and start upload
|
| 732 |
await safe_edit(st, f"β³ Batch {i}/{total}: starting uploadβ¦")
|
| 733 |
out = await _run_upload(uid)
|
| 734 |
|
| 735 |
-
if not out.get("ok"):
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
break
|
| 739 |
else:
|
| 740 |
await safe_edit(st, f"β Batch {i}/{total}: internal error (no pending).")
|
| 741 |
if not continue_on_fail:
|
| 742 |
break
|
| 743 |
|
| 744 |
batch_dur = max(0.001, time.time() - batch_start)
|
| 745 |
-
await safe_reply(m, f"β
Batch done in {batch_dur:.1f}s
|
| 746 |
|
| 747 |
t = asyncio.create_task(runner())
|
| 748 |
_BATCH_TASK[uid] = t
|
|
@@ -751,12 +972,27 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 751 |
@app.on_callback_query()
|
| 752 |
async def cb_handler(_, q: CallbackQuery) -> None:
|
| 753 |
uid = q.from_user.id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
action, value = parse_cb(q.data or "")
|
| 755 |
|
| 756 |
-
# Ignore noop
|
| 757 |
if action == "noop":
|
| 758 |
return
|
| 759 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
# Menus
|
| 761 |
if action == MENU_HELP:
|
| 762 |
await safe_edit(q.message, HELP_TEXT, reply_markup=main_menu_keyboard())
|
|
@@ -765,11 +1001,12 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 765 |
if action == MENU_SPEED:
|
| 766 |
if not await _ensure_allowed_uid(uid, q.message):
|
| 767 |
return
|
| 768 |
-
from bot.core.speedtest import net_download_test, net_upload_test
|
|
|
|
| 769 |
await safe_edit(q.message, "β± Running speed testβ¦")
|
| 770 |
dl = await net_download_test()
|
| 771 |
ul = await net_upload_test()
|
| 772 |
-
ip = await
|
| 773 |
|
| 774 |
dl_bps = float((dl or {}).get("bps", 0) or 0)
|
| 775 |
ul_bps = float((ul or {}).get("bps", 0) or 0)
|
|
@@ -788,6 +1025,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 788 |
if action == MENU_AUTH:
|
| 789 |
if not await _ensure_allowed_uid(uid, q.message):
|
| 790 |
return
|
|
|
|
| 791 |
await safe_edit(q.message, "π Add a profile:", reply_markup=auth_menu_keyboard())
|
| 792 |
return
|
| 793 |
|
|
@@ -804,9 +1042,40 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 804 |
return
|
| 805 |
|
| 806 |
if action == BACK:
|
|
|
|
| 807 |
await safe_edit(q.message, "π Menu", reply_markup=main_menu_keyboard())
|
| 808 |
return
|
| 809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
# Filename/Caption choice
|
| 811 |
if action in (NAME_ORIGINAL, NAME_CAPTION, NAME_CUSTOM):
|
| 812 |
p = _PENDING_UPLOAD.get(uid)
|
|
@@ -815,7 +1084,6 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 815 |
|
| 816 |
if action == NAME_ORIGINAL:
|
| 817 |
p.title = (p.title_from_filename or "Untitled")[: Settings.MAX_TITLE]
|
| 818 |
-
# keep caption as description (if present)
|
| 819 |
p.description = (p.caption_raw or "")[: Settings.MAX_DESC]
|
| 820 |
_PENDING_UPLOAD[uid] = p
|
| 821 |
await safe_edit(q.message, _render_preview(p), reply_markup=upload_confirm_keyboard(p.privacy))
|
|
@@ -826,7 +1094,6 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 826 |
p.title = p.title_from_caption[: Settings.MAX_TITLE]
|
| 827 |
p.description = p.desc_from_caption[: Settings.MAX_DESC]
|
| 828 |
else:
|
| 829 |
-
# no caption actually
|
| 830 |
p.title = (p.title_from_filename or "Untitled")[: Settings.MAX_TITLE]
|
| 831 |
p.description = ""
|
| 832 |
_PENDING_UPLOAD[uid] = p
|
|
@@ -880,7 +1147,11 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 880 |
if not p:
|
| 881 |
return
|
| 882 |
if _IN_PROGRESS.get(uid):
|
| 883 |
-
await safe_edit(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
return
|
| 885 |
p.status_msg = q.message
|
| 886 |
_PENDING_UPLOAD[uid] = p
|
|
@@ -927,13 +1198,22 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 927 |
await safe_edit(q.message, "β Failed to create login URL.", reply_markup=main_menu_keyboard())
|
| 928 |
return
|
| 929 |
|
| 930 |
-
# ----
|
| 931 |
@app.on_message(filters.text)
|
| 932 |
async def text_anywhere(_, m: Message) -> None:
|
| 933 |
if not m.from_user:
|
| 934 |
return
|
| 935 |
uid = m.from_user.id
|
| 936 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 937 |
# Confirm delete
|
| 938 |
if uid in _PENDING_DELETE:
|
| 939 |
if (m.text or "").strip().lower() == "yes":
|
|
@@ -950,7 +1230,7 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 950 |
|
| 951 |
# Edit title/desc
|
| 952 |
if uid in _AWAIT_EDIT:
|
| 953 |
-
|
| 954 |
p = _PENDING_UPLOAD.get(uid)
|
| 955 |
if not p:
|
| 956 |
return
|
|
@@ -965,7 +1245,6 @@ def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
|
| 965 |
|
| 966 |
_PENDING_UPLOAD[uid] = p
|
| 967 |
|
| 968 |
-
# Update preview in the current chat
|
| 969 |
if p.status_msg:
|
| 970 |
await safe_edit(p.status_msg, _render_preview(p), reply_markup=upload_confirm_keyboard(p.privacy))
|
| 971 |
else:
|
|
|
|
| 2 |
from __future__ import annotations
|
| 3 |
|
| 4 |
import asyncio
|
| 5 |
+
import json
|
| 6 |
import os
|
| 7 |
import re
|
| 8 |
+
import tempfile
|
| 9 |
import time
|
| 10 |
from dataclasses import dataclass
|
| 11 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 12 |
|
| 13 |
+
import httpx
|
| 14 |
from hydrogram import Client, filters
|
| 15 |
from hydrogram.types import CallbackQuery, Message
|
| 16 |
|
| 17 |
+
from bot.config import Auth, Telegram, Workers
|
| 18 |
+
from bot.core.progress import SpeedETA, human_bytes, human_eta
|
| 19 |
+
from bot.core.settings import Settings
|
| 20 |
+
from bot.core.uptime import uptime_text
|
| 21 |
from bot.integrations.auth import allow_user, disallow_user, get_stats, is_allowed
|
| 22 |
+
from bot.integrations.cf_worker1 import profile_add, profile_check_auth, profile_delete
|
| 23 |
+
from bot.integrations.cf_worker2 import (
|
| 24 |
+
get_default_profile,
|
| 25 |
+
list_profiles,
|
| 26 |
+
record_upload,
|
| 27 |
+
set_default_profile,
|
| 28 |
+
)
|
| 29 |
from bot.telegram.files import cleanup_file
|
| 30 |
from bot.telegram.media import download_to_temp
|
| 31 |
from bot.telegram.replies import safe_edit, safe_reply
|
| 32 |
from bot.ui.callbacks import (
|
| 33 |
+
AUTH_CI,
|
| 34 |
+
AUTH_JSON,
|
| 35 |
+
BACK,
|
| 36 |
+
CANCEL,
|
| 37 |
MENU_AUTH,
|
| 38 |
MENU_HELP,
|
| 39 |
MENU_PROFILES,
|
| 40 |
MENU_SPEED,
|
| 41 |
+
NAME_CAPTION,
|
| 42 |
+
NAME_CUSTOM,
|
| 43 |
+
NAME_ORIGINAL,
|
| 44 |
+
UP_CANCEL,
|
| 45 |
UP_DEL,
|
| 46 |
UP_EDIT,
|
| 47 |
UP_GO,
|
| 48 |
UP_PRIV,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
)
|
| 50 |
+
from bot.ui.keyboards import (
|
| 51 |
+
auth_menu_keyboard,
|
| 52 |
+
filename_keyboard,
|
| 53 |
+
main_menu_keyboard,
|
| 54 |
+
profiles_keyboard,
|
| 55 |
+
upload_confirm_keyboard,
|
| 56 |
+
)
|
| 57 |
from bot.ui.parse import parse_cb
|
| 58 |
+
from bot.ui.texts import CANCELLED, HELP_TEXT, NEED_AUTH, NOT_ALLOWED, OWNER_ONLY, START_TEXT
|
|
|
|
|
|
|
|
|
|
| 59 |
from bot.youtube.link_parser import parse_telegram_link
|
| 60 |
|
| 61 |
ChatRef = Union[int, str]
|
| 62 |
|
| 63 |
+
# ---- In-memory state ----
|
| 64 |
@dataclass
|
| 65 |
class PendingUpload:
|
| 66 |
request_msg: Message # where user triggered command / sent media
|
|
|
|
| 91 |
privacy: str
|
| 92 |
|
| 93 |
|
|
|
|
| 94 |
_PENDING_UPLOAD: Dict[int, PendingUpload] = {}
|
| 95 |
_AWAIT_EDIT: Dict[int, EditState] = {}
|
| 96 |
_PENDING_DELETE: Dict[int, str] = {}
|
|
|
|
| 98 |
_UPLOAD_TASK: Dict[int, asyncio.Task] = {}
|
| 99 |
_BATCH_TASK: Dict[int, asyncio.Task] = {}
|
| 100 |
|
| 101 |
+
# β
Auth input state (fixes: auth buttons "no response")
|
| 102 |
+
# value: "json" or "ci"
|
| 103 |
+
_AWAIT_AUTH: Dict[int, str] = {}
|
| 104 |
+
|
| 105 |
+
# Batch range cap (Settings may not define it yet)
|
| 106 |
+
_BATCH_MAX_RANGE = int(getattr(Settings, "BATCH_MAX_RANGE", 80) or 80)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _norm_ids(xs: Any) -> set[int]:
|
| 110 |
+
out: set[int] = set()
|
| 111 |
+
if not xs:
|
| 112 |
+
return out
|
| 113 |
+
if isinstance(xs, (int, float, str)):
|
| 114 |
+
xs = [xs]
|
| 115 |
+
for v in list(xs):
|
| 116 |
+
try:
|
| 117 |
+
s = str(v).strip()
|
| 118 |
+
if not s:
|
| 119 |
+
continue
|
| 120 |
+
out.add(int(float(s)))
|
| 121 |
+
except Exception:
|
| 122 |
+
continue
|
| 123 |
+
return out
|
| 124 |
+
|
| 125 |
|
| 126 |
def _is_admin_or_owner(uid: int) -> bool:
|
| 127 |
+
# Accept both Auth.* and Telegram.* and handle str/int mixes safely.
|
| 128 |
+
owners = _norm_ids(getattr(Auth, "OWNERS", None)) | _norm_ids(getattr(Telegram, "OWNER_ID", None))
|
| 129 |
+
admins = _norm_ids(getattr(Auth, "ADMINS", None)) | _norm_ids(getattr(Telegram, "ADMIN_IDS", None))
|
| 130 |
+
return uid in owners or uid in admins
|
| 131 |
|
| 132 |
|
| 133 |
async def _ensure_allowed_uid(uid: int, reply_target: Message) -> bool:
|
|
|
|
| 149 |
|
| 150 |
def _media_and_filename(m: Message) -> Tuple[Optional[Any], str, int]:
|
| 151 |
if m.video:
|
| 152 |
+
return m.video, (m.video.file_name or "video.mp4"), int(m.video.file_size or 0)
|
| 153 |
if m.document:
|
| 154 |
+
return m.document, (m.document.file_name or "file.bin"), int(m.document.file_size or 0)
|
| 155 |
return None, "", 0
|
| 156 |
|
| 157 |
|
|
|
|
| 289 |
return 0
|
| 290 |
|
| 291 |
|
| 292 |
+
async def _get_public_ip() -> Optional[str]:
|
| 293 |
+
# No extra dependency / no extra file needed
|
| 294 |
+
try:
|
| 295 |
+
async with httpx.AsyncClient(timeout=10) as c:
|
| 296 |
+
r = await c.get("https://api.ipify.org", params={"format": "text"})
|
| 297 |
+
if r.status_code < 400:
|
| 298 |
+
ip = (r.text or "").strip()
|
| 299 |
+
if ip and len(ip) <= 60:
|
| 300 |
+
return ip
|
| 301 |
+
except Exception:
|
| 302 |
+
pass
|
| 303 |
+
return None
|
| 304 |
+
|
| 305 |
+
|
| 306 |
async def _run_upload(uid: int) -> Dict[str, Any]:
|
| 307 |
"""
|
| 308 |
Runs one upload for uid, editing p.status_msg.
|
|
|
|
| 449 |
_PENDING_UPLOAD.pop(uid, None)
|
| 450 |
_AWAIT_EDIT.pop(uid, None)
|
| 451 |
_PENDING_DELETE.pop(uid, None)
|
| 452 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 453 |
if file_path:
|
| 454 |
try:
|
| 455 |
cleanup_file(file_path)
|
|
|
|
| 474 |
if not s:
|
| 475 |
raise ValueError("empty")
|
| 476 |
|
|
|
|
| 477 |
m = _BATCH_RANGE_RE.match(s)
|
| 478 |
if m:
|
| 479 |
base = m.group(1).strip()
|
|
|
|
| 484 |
if a > b:
|
| 485 |
a, b = b, a
|
| 486 |
|
|
|
|
| 487 |
single = f"{base}/{a}"
|
| 488 |
chat_ref, _mid = parse_telegram_link(single)
|
| 489 |
return chat_ref, a, b
|
| 490 |
|
|
|
|
| 491 |
chat_ref, mid = parse_telegram_link(s)
|
| 492 |
return chat_ref, int(mid), int(mid)
|
| 493 |
|
| 494 |
|
| 495 |
+
def _extract_client_secrets_from_json(obj: dict) -> Tuple[str, str]:
|
| 496 |
+
"""
|
| 497 |
+
Accepts client_secret.json formats:
|
| 498 |
+
{"installed": {"client_id": "...", "client_secret": "..."}}
|
| 499 |
+
{"web": {"client_id": "...", "client_secret": "..."}}
|
| 500 |
+
{"client_id": "...", "client_secret": "..."}
|
| 501 |
+
Returns (client_id, client_secret) or raises.
|
| 502 |
+
"""
|
| 503 |
+
if not isinstance(obj, dict):
|
| 504 |
+
raise ValueError("json_not_object")
|
| 505 |
+
|
| 506 |
+
for k in ("installed", "web"):
|
| 507 |
+
v = obj.get(k)
|
| 508 |
+
if isinstance(v, dict) and v.get("client_id") and v.get("client_secret"):
|
| 509 |
+
return str(v["client_id"]).strip(), str(v["client_secret"]).strip()
|
| 510 |
+
|
| 511 |
+
if obj.get("client_id") and obj.get("client_secret"):
|
| 512 |
+
return str(obj["client_id"]).strip(), str(obj["client_secret"]).strip()
|
| 513 |
+
|
| 514 |
+
raise ValueError("missing_client_id_or_secret")
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
async def _handle_auth_json_input(app: Client, m: Message) -> bool:
|
| 518 |
+
"""
|
| 519 |
+
Returns True if message was consumed as auth input.
|
| 520 |
+
"""
|
| 521 |
+
if not m.from_user:
|
| 522 |
+
return False
|
| 523 |
+
uid = m.from_user.id
|
| 524 |
+
if _AWAIT_AUTH.get(uid) != "json":
|
| 525 |
+
return False
|
| 526 |
+
|
| 527 |
+
if not await _ensure_allowed_uid(uid, m):
|
| 528 |
+
return True
|
| 529 |
+
|
| 530 |
+
# document .json
|
| 531 |
+
if m.document:
|
| 532 |
+
name = (m.document.file_name or "").lower()
|
| 533 |
+
if not name.endswith(".json"):
|
| 534 |
+
await safe_reply(m, "β Please send a `.json` file (Google client_secret.json). Or paste JSON text.")
|
| 535 |
+
return True
|
| 536 |
+
|
| 537 |
+
fd, path = tempfile.mkstemp(prefix="yt_auth_", suffix=".json")
|
| 538 |
+
os.close(fd)
|
| 539 |
+
try:
|
| 540 |
+
await app.download_media(message=m, file_name=path)
|
| 541 |
+
raw = ""
|
| 542 |
+
try:
|
| 543 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 544 |
+
raw = f.read()
|
| 545 |
+
except Exception:
|
| 546 |
+
with open(path, "rb") as f:
|
| 547 |
+
raw = f.read().decode("utf-8", errors="ignore")
|
| 548 |
+
|
| 549 |
+
obj = json.loads(raw)
|
| 550 |
+
client_id, client_secret = _extract_client_secrets_from_json(obj)
|
| 551 |
+
|
| 552 |
+
out = await profile_add(uid, client_id=client_id, client_secret=client_secret, label="main")
|
| 553 |
+
if isinstance(out, dict) and out.get("ok"):
|
| 554 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 555 |
+
await safe_reply(
|
| 556 |
+
m,
|
| 557 |
+
"β
Profile added.\n\nNow run /profiles β *Login* β *Set default*.",
|
| 558 |
+
reply_markup=main_menu_keyboard(),
|
| 559 |
+
)
|
| 560 |
+
else:
|
| 561 |
+
err = (out or {}).get("err") if isinstance(out, dict) else "profile_add_failed"
|
| 562 |
+
await safe_reply(m, f"β Failed to add profile: `{err}`")
|
| 563 |
+
return True
|
| 564 |
+
except Exception as e:
|
| 565 |
+
await safe_reply(m, f"β JSON parse/add failed: `{str(e)[:220]}`")
|
| 566 |
+
return True
|
| 567 |
+
finally:
|
| 568 |
+
try:
|
| 569 |
+
if os.path.exists(path):
|
| 570 |
+
os.remove(path)
|
| 571 |
+
except Exception:
|
| 572 |
+
pass
|
| 573 |
+
|
| 574 |
+
# pasted JSON
|
| 575 |
+
txt = (m.text or "").strip()
|
| 576 |
+
if txt.startswith("{") and txt.endswith("}"):
|
| 577 |
+
try:
|
| 578 |
+
obj = json.loads(txt)
|
| 579 |
+
client_id, client_secret = _extract_client_secrets_from_json(obj)
|
| 580 |
+
|
| 581 |
+
out = await profile_add(uid, client_id=client_id, client_secret=client_secret, label="main")
|
| 582 |
+
if isinstance(out, dict) and out.get("ok"):
|
| 583 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 584 |
+
await safe_reply(
|
| 585 |
+
m,
|
| 586 |
+
"β
Profile added.\n\nNow run /profiles β *Login* β *Set default*.",
|
| 587 |
+
reply_markup=main_menu_keyboard(),
|
| 588 |
+
)
|
| 589 |
+
else:
|
| 590 |
+
err = (out or {}).get("err") if isinstance(out, dict) else "profile_add_failed"
|
| 591 |
+
await safe_reply(m, f"β Failed to add profile: `{err}`")
|
| 592 |
+
except Exception as e:
|
| 593 |
+
await safe_reply(m, f"β JSON parse/add failed: `{str(e)[:220]}`")
|
| 594 |
+
return True
|
| 595 |
+
|
| 596 |
+
await safe_reply(m, "π Send the `.json` file or paste JSON text (client_secret.json).")
|
| 597 |
+
return True
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
async def _handle_auth_ci_input(m: Message) -> bool:
|
| 601 |
+
"""
|
| 602 |
+
Returns True if message was consumed as auth input.
|
| 603 |
+
"""
|
| 604 |
+
if not m.from_user:
|
| 605 |
+
return False
|
| 606 |
+
uid = m.from_user.id
|
| 607 |
+
if _AWAIT_AUTH.get(uid) != "ci":
|
| 608 |
+
return False
|
| 609 |
+
|
| 610 |
+
if not await _ensure_allowed_uid(uid, m):
|
| 611 |
+
return True
|
| 612 |
+
|
| 613 |
+
txt = (m.text or "").strip()
|
| 614 |
+
if not txt:
|
| 615 |
+
await safe_reply(m, "Send:\n`<CLIENT_ID>`\n`<CLIENT_SECRET>`\n(Optional 3rd line: label)")
|
| 616 |
+
return True
|
| 617 |
+
|
| 618 |
+
lines = [ln.strip() for ln in txt.splitlines() if ln.strip()]
|
| 619 |
+
if len(lines) == 1:
|
| 620 |
+
# maybe space separated
|
| 621 |
+
parts = [p for p in lines[0].split() if p.strip()]
|
| 622 |
+
if len(parts) >= 2:
|
| 623 |
+
client_id, client_secret = parts[0], parts[1]
|
| 624 |
+
label = parts[2] if len(parts) >= 3 else "main"
|
| 625 |
+
else:
|
| 626 |
+
await safe_reply(m, "Send:\n`<CLIENT_ID>`\n`<CLIENT_SECRET>`\n(Optional 3rd line: label)")
|
| 627 |
+
return True
|
| 628 |
+
else:
|
| 629 |
+
client_id = lines[0]
|
| 630 |
+
client_secret = lines[1] if len(lines) >= 2 else ""
|
| 631 |
+
label = lines[2] if len(lines) >= 3 else "main"
|
| 632 |
+
|
| 633 |
+
out = await profile_add(uid, client_id=client_id, client_secret=client_secret, label=label)
|
| 634 |
+
if isinstance(out, dict) and out.get("ok"):
|
| 635 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 636 |
+
await safe_reply(
|
| 637 |
+
m,
|
| 638 |
+
f"β
Profile added (label: `{label}`).\n\nNow run /profiles β *Login* β *Set default*.",
|
| 639 |
+
reply_markup=main_menu_keyboard(),
|
| 640 |
+
)
|
| 641 |
+
else:
|
| 642 |
+
err = (out or {}).get("err") if isinstance(out, dict) else "profile_add_failed"
|
| 643 |
+
await safe_reply(m, f"β Failed to add profile: `{err}`")
|
| 644 |
+
return True
|
| 645 |
+
|
| 646 |
+
|
| 647 |
def setup_handlers(app: Client, user_app: Optional[Client]) -> None:
|
| 648 |
# ---- basic ----
|
| 649 |
@app.on_message(filters.command(["start", "help"]))
|
| 650 |
async def start_help_cmd(_, m: Message) -> None:
|
|
|
|
| 651 |
if (m.text or "").strip().lower().startswith("/help"):
|
| 652 |
await safe_reply(m, HELP_TEXT, reply_markup=main_menu_keyboard())
|
| 653 |
else:
|
|
|
|
| 654 |
await safe_reply(m, START_TEXT, reply_markup=main_menu_keyboard())
|
| 655 |
|
| 656 |
@app.on_message(filters.command("speedtest"))
|
|
|
|
| 658 |
uid = m.from_user.id if m.from_user else 0
|
| 659 |
if not await _ensure_allowed_uid(uid, m):
|
| 660 |
return
|
| 661 |
+
from bot.core.speedtest import net_download_test, net_upload_test
|
| 662 |
+
|
| 663 |
+
st = await safe_reply(m, "β± Running speed testβ¦")
|
| 664 |
dl = await net_download_test()
|
| 665 |
ul = await net_upload_test()
|
| 666 |
+
ip = await _get_public_ip()
|
| 667 |
|
| 668 |
dl_bps = float((dl or {}).get("bps", 0) or 0)
|
| 669 |
ul_bps = float((ul or {}).get("bps", 0) or 0)
|
|
|
|
| 675 |
f"*Download:* `{human_bytes(dl_bps)}/s`\n"
|
| 676 |
f"*Upload:* `{human_bytes(ul_bps)}/s`"
|
| 677 |
)
|
| 678 |
+
if st:
|
| 679 |
+
await safe_edit(st, msg, reply_markup=main_menu_keyboard())
|
| 680 |
+
else:
|
| 681 |
+
await safe_reply(m, msg, reply_markup=main_menu_keyboard())
|
| 682 |
|
| 683 |
@app.on_message(filters.command("cancel"))
|
| 684 |
async def cancel_cmd(_, m: Message) -> None:
|
| 685 |
+
uid = m.from_user.id if m.from_user else 0
|
| 686 |
+
|
| 687 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 688 |
_AWAIT_EDIT.pop(uid, None)
|
| 689 |
_PENDING_UPLOAD.pop(uid, None)
|
| 690 |
_PENDING_DELETE.pop(uid, None)
|
|
|
|
| 704 |
# ---- admin allowlist ----
|
| 705 |
@app.on_message(filters.command("allow"))
|
| 706 |
async def allow_cmd(_, m: Message) -> None:
|
| 707 |
+
uid = m.from_user.id if m.from_user else 0
|
| 708 |
if not _is_admin_or_owner(uid):
|
| 709 |
await safe_reply(m, OWNER_ONLY)
|
| 710 |
return
|
|
|
|
| 722 |
|
| 723 |
@app.on_message(filters.command("disallow"))
|
| 724 |
async def disallow_cmd(_, m: Message) -> None:
|
| 725 |
+
uid = m.from_user.id if m.from_user else 0
|
| 726 |
if not _is_admin_or_owner(uid):
|
| 727 |
await safe_reply(m, OWNER_ONLY)
|
| 728 |
return
|
|
|
|
| 740 |
|
| 741 |
@app.on_message(filters.command("stats"))
|
| 742 |
async def stats_cmd(_, m: Message) -> None:
|
| 743 |
+
uid = m.from_user.id if m.from_user else 0
|
| 744 |
if not _is_admin_or_owner(uid):
|
| 745 |
await safe_reply(m, OWNER_ONLY)
|
| 746 |
return
|
|
|
|
| 758 |
|
| 759 |
@app.on_message(filters.command("diag"))
|
| 760 |
async def diag_cmd(_, m: Message) -> None:
|
| 761 |
+
uid = m.from_user.id if m.from_user else 0
|
| 762 |
if not _is_admin_or_owner(uid):
|
| 763 |
await safe_reply(m, OWNER_ONLY)
|
| 764 |
return
|
|
|
|
| 772 |
f"Uptime: `{uptime_text()}`\n"
|
| 773 |
f"WORKER1_URL: `{w1}`\n"
|
| 774 |
f"WORKER2_URL: `{w2}`\n"
|
| 775 |
+
f"Owners: `{len(_norm_ids(getattr(Auth, 'OWNERS', None)) | _norm_ids(getattr(Telegram, 'OWNER_ID', None)))}` "
|
| 776 |
+
f"Admins: `{len(_norm_ids(getattr(Auth, 'ADMINS', None)) | _norm_ids(getattr(Telegram, 'ADMIN_IDS', None)))}`",
|
| 777 |
)
|
| 778 |
|
| 779 |
# ---- auth/profile flow ----
|
| 780 |
@app.on_message(filters.command("auth"))
|
| 781 |
async def auth_cmd(_, m: Message) -> None:
|
| 782 |
+
uid = m.from_user.id if m.from_user else 0
|
| 783 |
if not await _ensure_allowed_uid(uid, m):
|
| 784 |
return
|
| 785 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 786 |
await safe_reply(m, "π Add a profile:", reply_markup=auth_menu_keyboard())
|
| 787 |
|
| 788 |
@app.on_message(filters.command("profiles"))
|
| 789 |
async def profiles_cmd(_, m: Message) -> None:
|
| 790 |
+
uid = m.from_user.id if m.from_user else 0
|
| 791 |
if not await _ensure_allowed_uid(uid, m):
|
| 792 |
return
|
| 793 |
data = await list_profiles(uid, only_connected=False)
|
|
|
|
| 798 |
default_id = data.get("default_profile_id") or ""
|
| 799 |
await safe_reply(m, "π€ *Profiles*", reply_markup=profiles_keyboard(profiles, default_id))
|
| 800 |
|
| 801 |
+
# ---- upload from DM media / OR auth-json doc handling ----
|
| 802 |
@app.on_message(filters.private & (filters.video | filters.document))
|
| 803 |
async def media_in_dm(_, m: Message) -> None:
|
| 804 |
+
if not m.from_user:
|
| 805 |
+
return
|
| 806 |
uid = m.from_user.id
|
| 807 |
+
|
| 808 |
+
# β
If we're waiting for auth JSON, consume json file instead of treating it as "upload media"
|
| 809 |
+
if await _handle_auth_json_input(app, m):
|
| 810 |
+
return
|
| 811 |
+
|
| 812 |
if not await _ensure_allowed_uid(uid, m):
|
| 813 |
return
|
| 814 |
if _IN_PROGRESS.get(uid):
|
|
|
|
| 819 |
# ---- upload from link (admin/owner) ----
|
| 820 |
@app.on_message(filters.command(["yt", "dl", "archive"]))
|
| 821 |
async def archive_cmd(_, m: Message) -> None:
|
| 822 |
+
if not m.from_user:
|
| 823 |
+
return
|
| 824 |
uid = m.from_user.id
|
| 825 |
if not await _ensure_allowed_uid(uid, m):
|
| 826 |
return
|
|
|
|
| 858 |
# ---- batch mode ----
|
| 859 |
@app.on_message(filters.command("batch"))
|
| 860 |
async def batch_cmd(_, m: Message) -> None:
|
| 861 |
+
if not m.from_user:
|
| 862 |
+
return
|
| 863 |
uid = m.from_user.id
|
| 864 |
if not await _ensure_allowed_uid(uid, m):
|
| 865 |
return
|
|
|
|
| 889 |
await safe_reply(m, "No links found. Put one t.me link per line after /batch")
|
| 890 |
return
|
| 891 |
|
|
|
|
| 892 |
items: List[Tuple[ChatRef, int]] = []
|
| 893 |
for ln in lines:
|
| 894 |
try:
|
|
|
|
| 900 |
continue
|
| 901 |
|
| 902 |
count = (b - a + 1)
|
| 903 |
+
if count > _BATCH_MAX_RANGE:
|
| 904 |
+
await safe_reply(m, f"β Range too large ({count}). Max is {_BATCH_MAX_RANGE}.")
|
| 905 |
if not continue_on_fail:
|
| 906 |
return
|
| 907 |
continue
|
|
|
|
| 913 |
await safe_reply(m, "No valid items to process.")
|
| 914 |
return
|
| 915 |
|
| 916 |
+
await safe_reply(
|
| 917 |
+
m,
|
| 918 |
+
f"π§Ύ Batch starting: {len(items)} item(s).\nMode: `{'continue' if continue_on_fail else 'stop_on_fail'}`",
|
| 919 |
+
)
|
| 920 |
|
| 921 |
async def runner() -> None:
|
| 922 |
batch_start = time.time()
|
| 923 |
total = len(items)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 924 |
|
| 925 |
+
for i, (chat_ref, msg_id) in enumerate(items, 1):
|
| 926 |
st = await safe_reply(m, f"π Batch {i}/{total}: fetching message `{msg_id}`β¦")
|
| 927 |
if not st:
|
| 928 |
if not continue_on_fail:
|
|
|
|
| 937 |
break
|
| 938 |
continue
|
| 939 |
|
| 940 |
+
media, _, _ = _media_and_filename(src)
|
| 941 |
if not media:
|
| 942 |
await safe_edit(st, f"β Batch {i}/{total}: no media in that message. Skipped.")
|
| 943 |
continue
|
|
|
|
| 945 |
# Create pending upload targeting THIS chat (m), not source chat
|
| 946 |
await _start_pending_upload(uid=uid, request_msg=m, src_msg=src, downloader=user_app, via_link=True)
|
| 947 |
|
| 948 |
+
# Force progress to edit THIS batch line
|
| 949 |
p = _PENDING_UPLOAD.get(uid)
|
| 950 |
if p:
|
| 951 |
p.status_msg = st
|
| 952 |
_PENDING_UPLOAD[uid] = p
|
| 953 |
|
|
|
|
| 954 |
await safe_edit(st, f"β³ Batch {i}/{total}: starting uploadβ¦")
|
| 955 |
out = await _run_upload(uid)
|
| 956 |
|
| 957 |
+
if not out.get("ok") and not continue_on_fail:
|
| 958 |
+
await safe_reply(m, f"π Batch stopped (failed at {i}/{total}).")
|
| 959 |
+
break
|
|
|
|
| 960 |
else:
|
| 961 |
await safe_edit(st, f"β Batch {i}/{total}: internal error (no pending).")
|
| 962 |
if not continue_on_fail:
|
| 963 |
break
|
| 964 |
|
| 965 |
batch_dur = max(0.001, time.time() - batch_start)
|
| 966 |
+
await safe_reply(m, f"β
Batch done in `{batch_dur:.1f}s`.", reply_markup=main_menu_keyboard())
|
| 967 |
|
| 968 |
t = asyncio.create_task(runner())
|
| 969 |
_BATCH_TASK[uid] = t
|
|
|
|
| 972 |
@app.on_callback_query()
|
| 973 |
async def cb_handler(_, q: CallbackQuery) -> None:
|
| 974 |
uid = q.from_user.id
|
| 975 |
+
|
| 976 |
+
# β
Stop Telegram loading spinner
|
| 977 |
+
try:
|
| 978 |
+
await q.answer()
|
| 979 |
+
except Exception:
|
| 980 |
+
pass
|
| 981 |
+
|
| 982 |
action, value = parse_cb(q.data or "")
|
| 983 |
|
|
|
|
| 984 |
if action == "noop":
|
| 985 |
return
|
| 986 |
|
| 987 |
+
# Cancel from keyboards (used in filename keyboard, and we support it globally)
|
| 988 |
+
if action == CANCEL:
|
| 989 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 990 |
+
_AWAIT_EDIT.pop(uid, None)
|
| 991 |
+
_PENDING_UPLOAD.pop(uid, None)
|
| 992 |
+
_PENDING_DELETE.pop(uid, None)
|
| 993 |
+
await safe_edit(q.message, CANCELLED, reply_markup=main_menu_keyboard())
|
| 994 |
+
return
|
| 995 |
+
|
| 996 |
# Menus
|
| 997 |
if action == MENU_HELP:
|
| 998 |
await safe_edit(q.message, HELP_TEXT, reply_markup=main_menu_keyboard())
|
|
|
|
| 1001 |
if action == MENU_SPEED:
|
| 1002 |
if not await _ensure_allowed_uid(uid, q.message):
|
| 1003 |
return
|
| 1004 |
+
from bot.core.speedtest import net_download_test, net_upload_test
|
| 1005 |
+
|
| 1006 |
await safe_edit(q.message, "β± Running speed testβ¦")
|
| 1007 |
dl = await net_download_test()
|
| 1008 |
ul = await net_upload_test()
|
| 1009 |
+
ip = await _get_public_ip()
|
| 1010 |
|
| 1011 |
dl_bps = float((dl or {}).get("bps", 0) or 0)
|
| 1012 |
ul_bps = float((ul or {}).get("bps", 0) or 0)
|
|
|
|
| 1025 |
if action == MENU_AUTH:
|
| 1026 |
if not await _ensure_allowed_uid(uid, q.message):
|
| 1027 |
return
|
| 1028 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 1029 |
await safe_edit(q.message, "π Add a profile:", reply_markup=auth_menu_keyboard())
|
| 1030 |
return
|
| 1031 |
|
|
|
|
| 1042 |
return
|
| 1043 |
|
| 1044 |
if action == BACK:
|
| 1045 |
+
_AWAIT_AUTH.pop(uid, None)
|
| 1046 |
await safe_edit(q.message, "π Menu", reply_markup=main_menu_keyboard())
|
| 1047 |
return
|
| 1048 |
|
| 1049 |
+
# β
AUTH buttons (THIS is what was missing -> no response)
|
| 1050 |
+
if action == AUTH_JSON:
|
| 1051 |
+
if not await _ensure_allowed_uid(uid, q.message):
|
| 1052 |
+
return
|
| 1053 |
+
_AWAIT_AUTH[uid] = "json"
|
| 1054 |
+
await safe_edit(
|
| 1055 |
+
q.message,
|
| 1056 |
+
"π *Send JSON credentials*\n\n"
|
| 1057 |
+
"Send `client_secret.json` as a file OR paste the JSON text here.\n\n"
|
| 1058 |
+
"Use /cancel to stop.",
|
| 1059 |
+
reply_markup=auth_menu_keyboard(),
|
| 1060 |
+
)
|
| 1061 |
+
return
|
| 1062 |
+
|
| 1063 |
+
if action == AUTH_CI:
|
| 1064 |
+
if not await _ensure_allowed_uid(uid, q.message):
|
| 1065 |
+
return
|
| 1066 |
+
_AWAIT_AUTH[uid] = "ci"
|
| 1067 |
+
await safe_edit(
|
| 1068 |
+
q.message,
|
| 1069 |
+
"π *Send Client ID + Secret*\n\n"
|
| 1070 |
+
"Send like this:\n"
|
| 1071 |
+
"`CLIENT_ID`\n"
|
| 1072 |
+
"`CLIENT_SECRET`\n"
|
| 1073 |
+
"(Optional 3rd line: label)\n\n"
|
| 1074 |
+
"Use /cancel to stop.",
|
| 1075 |
+
reply_markup=auth_menu_keyboard(),
|
| 1076 |
+
)
|
| 1077 |
+
return
|
| 1078 |
+
|
| 1079 |
# Filename/Caption choice
|
| 1080 |
if action in (NAME_ORIGINAL, NAME_CAPTION, NAME_CUSTOM):
|
| 1081 |
p = _PENDING_UPLOAD.get(uid)
|
|
|
|
| 1084 |
|
| 1085 |
if action == NAME_ORIGINAL:
|
| 1086 |
p.title = (p.title_from_filename or "Untitled")[: Settings.MAX_TITLE]
|
|
|
|
| 1087 |
p.description = (p.caption_raw or "")[: Settings.MAX_DESC]
|
| 1088 |
_PENDING_UPLOAD[uid] = p
|
| 1089 |
await safe_edit(q.message, _render_preview(p), reply_markup=upload_confirm_keyboard(p.privacy))
|
|
|
|
| 1094 |
p.title = p.title_from_caption[: Settings.MAX_TITLE]
|
| 1095 |
p.description = p.desc_from_caption[: Settings.MAX_DESC]
|
| 1096 |
else:
|
|
|
|
| 1097 |
p.title = (p.title_from_filename or "Untitled")[: Settings.MAX_TITLE]
|
| 1098 |
p.description = ""
|
| 1099 |
_PENDING_UPLOAD[uid] = p
|
|
|
|
| 1147 |
if not p:
|
| 1148 |
return
|
| 1149 |
if _IN_PROGRESS.get(uid):
|
| 1150 |
+
await safe_edit(
|
| 1151 |
+
q.message,
|
| 1152 |
+
"β³ Upload already running. Use /cancel to stop.",
|
| 1153 |
+
reply_markup=main_menu_keyboard(),
|
| 1154 |
+
)
|
| 1155 |
return
|
| 1156 |
p.status_msg = q.message
|
| 1157 |
_PENDING_UPLOAD[uid] = p
|
|
|
|
| 1198 |
await safe_edit(q.message, "β Failed to create login URL.", reply_markup=main_menu_keyboard())
|
| 1199 |
return
|
| 1200 |
|
| 1201 |
+
# ---- text handler ----
|
| 1202 |
@app.on_message(filters.text)
|
| 1203 |
async def text_anywhere(_, m: Message) -> None:
|
| 1204 |
if not m.from_user:
|
| 1205 |
return
|
| 1206 |
uid = m.from_user.id
|
| 1207 |
|
| 1208 |
+
# β
Auth CI input
|
| 1209 |
+
if await _handle_auth_ci_input(m):
|
| 1210 |
+
return
|
| 1211 |
+
|
| 1212 |
+
# β
Auth JSON pasted input (when user pastes text)
|
| 1213 |
+
if _AWAIT_AUTH.get(uid) == "json":
|
| 1214 |
+
if await _handle_auth_json_input(app, m):
|
| 1215 |
+
return
|
| 1216 |
+
|
| 1217 |
# Confirm delete
|
| 1218 |
if uid in _PENDING_DELETE:
|
| 1219 |
if (m.text or "").strip().lower() == "yes":
|
|
|
|
| 1230 |
|
| 1231 |
# Edit title/desc
|
| 1232 |
if uid in _AWAIT_EDIT:
|
| 1233 |
+
_ = _AWAIT_EDIT.pop(uid)
|
| 1234 |
p = _PENDING_UPLOAD.get(uid)
|
| 1235 |
if not p:
|
| 1236 |
return
|
|
|
|
| 1245 |
|
| 1246 |
_PENDING_UPLOAD[uid] = p
|
| 1247 |
|
|
|
|
| 1248 |
if p.status_msg:
|
| 1249 |
await safe_edit(p.status_msg, _render_preview(p), reply_markup=upload_confirm_keyboard(p.privacy))
|
| 1250 |
else:
|