"""Turn the many ways of naming a sticker pack into a bare pack name.""" from __future__ import annotations import re from collections.abc import Iterable from pathlib import Path from urllib.parse import parse_qs, urlparse from sticker_downloader.errors import InvalidPackReference #: Telegram pack names are ASCII letters, digits and underscores. _PACK_NAME_RE = re.compile(r"^[A-Za-z0-9_]{1,64}$") #: Hosts that serve ``/addstickers/`` links. _KNOWN_HOSTS = frozenset({"t.me", "telegram.me", "telegram.dog", "telesco.pe"}) _ADD_STICKERS_PATHS = frozenset({"addstickers", "addemoji"}) def parse_pack_name(reference: str) -> str: """Extract the pack name from anything a user is likely to paste. Accepts full URLs (``https://t.me/addstickers/Foo``), scheme-less links (``t.me/addstickers/Foo``), ``tg://addstickers?set=Foo`` deep links, a bare ``addstickers/Foo`` path, and a plain pack name. Raises: InvalidPackReference: if no pack name can be recovered. """ text = (reference or "").strip().strip('"').strip("'") if not text: raise InvalidPackReference("empty sticker pack reference") if text.lower().startswith("tg://"): return _validate(_name_from_deep_link(text), reference) if "/" not in text: return _validate(text, reference) return _validate(_name_from_url(text, reference), reference) def _name_from_deep_link(text: str) -> str: parsed = urlparse(text) query = parse_qs(parsed.query) for key in ("set", "name"): values = query.get(key) if values and values[0].strip(): return values[0].strip() return "" def _name_from_url(text: str, reference: str) -> str: candidate = text if "//" in text else f"https://{text}" parsed = urlparse(candidate) segments = [segment for segment in parsed.path.split("/") if segment] host = parsed.netloc.lower().removeprefix("www.").split(":")[0] if host in _ADD_STICKERS_PATHS: # A path-only reference such as "addstickers/Foo". return segments[0] if segments else "" if host not in _KNOWN_HOSTS: raise InvalidPackReference( f"{reference!r} is not a Telegram sticker link " "(expected something like https://t.me/addstickers/PackName)" ) if len(segments) < 2 or segments[0].lower() not in _ADD_STICKERS_PATHS: raise InvalidPackReference(f"{reference!r} is not an /addstickers/ link") return segments[1] def _validate(name: str, reference: str) -> str: name = name.strip() if not _PACK_NAME_RE.match(name): raise InvalidPackReference( f"{reference!r} does not contain a valid pack name " "(letters, digits and underscores only)" ) return name def parse_pack_reference_lines( lines: Iterable[str], source: str = "input" ) -> tuple[list[str], list[str]]: """Parse pack references from lines of text. Blank lines and ``#`` comments are ignored. Duplicate references collapse to the first occurrence so a pack is never downloaded twice in one run. Returns: ``(references, problems)`` — valid references in input order, plus a human-readable message for every line that could not be parsed. """ references: list[str] = [] problems: list[str] = [] seen: set[str] = set() for line_number, raw_line in enumerate(lines, start=1): line = raw_line.strip() if not line or line.startswith("#"): continue try: name = parse_pack_name(line) except InvalidPackReference as error: problems.append(f"{source}:{line_number}: {error}") continue if name.lower() in seen: continue seen.add(name.lower()) references.append(line) return references, problems def read_pack_references(path: Path | str) -> tuple[list[str], list[str]]: """Read pack references from a text file, one per line. See :func:`parse_pack_reference_lines` for the accepted syntax. """ file_path = Path(path) lines = file_path.read_text(encoding="utf-8").splitlines() return parse_pack_reference_lines(lines, source=str(file_path))