feat(api,userbot,frontend): search peers without chats and start tracking them

This commit is contained in:
hh
2026-08-05 23:46:54 +02:00
parent dcf95bd9d4
commit 92fd20137e
21 changed files with 936 additions and 65 deletions
@@ -0,0 +1,102 @@
import re
from pyrogram import Client, raw
from pyrogram.errors import BadRequest, Forbidden
from pyrogram.types import Chat
from userbot.modules.capture.context import CaptureContext
from userbot.modules.jobs.context import JobContext
from userbot.modules.jobs.registry import register
from userbot.modules.profiles.snapshots import save_chat
DEFAULT_LIMIT = 30
_USERNAME = re.compile(r"^[a-z][a-z0-9_]{3,31}$", re.IGNORECASE)
_PREFIXES = ("https://t.me/", "http://t.me/", "t.me/", "@")
def _normalize(query: str) -> str:
text = query.strip()
for prefix in _PREFIXES:
if text.lower().startswith(prefix):
text = text[len(prefix) :]
break
return text.strip("/")
_SOURCE_TYPES = (raw.types.User, raw.types.Chat, raw.types.Channel)
def _source(
peer: raw.base.Peer, users: dict, chats: dict
) -> raw.types.User | raw.types.Chat | raw.types.Channel | None:
if isinstance(peer, raw.types.PeerUser):
source = users.get(peer.user_id)
elif isinstance(peer, raw.types.PeerChannel):
source = chats.get(peer.channel_id)
elif isinstance(peer, raw.types.PeerChat):
source = chats.get(peer.chat_id)
else:
return None
return source if isinstance(source, _SOURCE_TYPES) else None
async def _save_found(
client: Client, ctx: CaptureContext, peer: raw.base.Peer, users: dict, chats: dict
) -> int | None:
source = _source(peer, users, chats)
if source is None:
return None
chat = Chat._parse_chat(client, source) # noqa: SLF001
if chat is None or chat.id is None:
return None
await save_chat(ctx, chat)
return chat.id
async def _resolve(client: Client, ctx: CaptureContext, query: str) -> int | None:
try:
chat = await client.get_chat(query)
except (BadRequest, Forbidden):
return None
if not isinstance(chat, Chat) or chat.id is None:
return None
await save_chat(ctx, chat)
return chat.id
async def _search(
client: Client, query: str, limit: int
) -> raw.base.contacts.Found | None:
try:
return await client.invoke(raw.functions.contacts.Search(q=query, limit=limit))
except (BadRequest, Forbidden):
return None
@register("search_peers")
async def search_peers(ctx: JobContext) -> None:
client = ctx.client
if client is None:
return
capture = getattr(client, "capture", None)
if capture is None:
return
query = _normalize(ctx.job.params.get("query", ""))
if not query:
await ctx.report_progress({"ids": [], "done": True})
return
limit = int(ctx.job.params.get("limit", DEFAULT_LIMIT))
found = await _search(client, query, limit)
ids: list[int] = []
if found is not None:
users = {user.id: user for user in found.users}
chats = {chat.id: chat for chat in found.chats}
for peer in (*found.my_results, *found.results):
peer_id = await _save_found(client, capture, peer, users, chats)
if peer_id is not None and peer_id not in ids:
ids.append(peer_id)
if _USERNAME.match(query):
resolved = await _resolve(client, capture, query)
if resolved is not None and resolved not in ids:
ids.insert(0, resolved)
await ctx.report_progress({"ids": ids, "done": True})