126 lines
4.1 KiB
Python
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))
|