Files
2026-07-27 15:16:01 +02:00

126 lines
4.1 KiB
Python

"""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/<name>`` 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))