"""Parse and validate Telegram message links. Supported: https://t.me/c/3909337424/14 private/internal channel link https://t.me/c/3909337424/7/14 same, with a discussion-thread segment https://t.me/channelname/14 public channel post https://t.me/s/channelname/14 public web-preview link Variants with ``www.``, ``telegram.me``, no scheme, a trailing slash, query parameters (``?single``) or a fragment are accepted. For ``/c//`` links the number is the channel's *internal* ID. The MTProto "marked" peer ID used by Telethon is ``-(10**12 + id)`` (for 10-digit IDs this looks like ``-100`` followed by the ID). It is only an identifier: access is still verified by resolving it through the authenticated client. """ from __future__ import annotations import re from dataclasses import dataclass from urllib.parse import urlsplit _HOSTS = {"t.me", "telegram.me", "telegram.dog"} _USERNAME = re.compile(r"^[A-Za-z][A-Za-z0-9_]{3,31}$") # Paths on t.me that are not channel usernames. _RESERVED = { "addstickers", "addemoji", "addtheme", "addlist", "boost", "bg", "share", "proxy", "socks", "iv", "login", "setlanguage", "invoice", "joinchat", "contact", "m", "confirmphone", "giftcode", } EXAMPLE = "https://t.me/c/3909337424/14" class LinkError(ValueError): """Invalid link; the message is safe to show to the user.""" def marked_channel_id(internal_id: int) -> int: return -(10**12 + internal_id) def internal_channel_id(marked_id: int) -> int: return -marked_id - 10**12 @dataclass(frozen=True) class MessageLink: kind: str # "private" or "public" channel: str # internal numeric ID (private) or username (public) message_id: int url: str @property def channel_ref(self) -> str: """Reference understood by telegram_client.resolve_channel.""" if self.kind == "private": return str(marked_channel_id(int(self.channel))) return self.channel @property def same_channel_key(self) -> str: return self.channel if self.kind == "private" else self.channel.lower() def parse_message_link(text: str | None) -> MessageLink: raw = (text or "").strip() if not raw: raise LinkError(f"Please send a Telegram message link, for example {EXAMPLE}") if any(ch.isspace() for ch in raw): raise LinkError("Send only the link, without any other text.") url = raw if re.match(r"^[a-z]+://", raw, re.IGNORECASE) else "https://" + raw try: parts = urlsplit(url) except ValueError as exc: raise LinkError(f"That is not a valid URL. Example: {EXAMPLE}") from exc host = (parts.hostname or "").lower() if host.startswith("www."): host = host[4:] if parts.scheme.lower() not in ("http", "https") or host not in _HOSTS: raise LinkError(f"That is not a Telegram (t.me) link. Example: {EXAMPLE}") segments = [s for s in parts.path.split("/") if s] if not segments: raise LinkError(f"The link does not point to a message. Example: {EXAMPLE}") head = segments[0] if head.startswith("+") or head.lower() == "joinchat": raise LinkError("Invite links cannot be used. Open the channel, then copy the link of a specific post.") if head == "c": if len(segments) not in (3, 4) or not all(s.isdigit() for s in segments[1:]): raise LinkError(f"Private-channel links must look like {EXAMPLE} (channel ID, then message ID).") channel, message = segments[1], segments[-1] kind = "private" if int(channel) == 0: raise LinkError("The channel ID in the link is invalid.") else: if head == "s": segments = segments[1:] if len(segments) == 1: raise LinkError("This link points to a channel, not to a message. Copy the link of a specific post.") if len(segments) not in (2, 3) or not segments[-1].isdigit(): raise LinkError(f"Unsupported Telegram link format. Use a post link such as {EXAMPLE}") channel, message = segments[0], segments[-1] if channel.lower() in _RESERVED or not _USERNAME.match(channel): raise LinkError("Unsupported Telegram link type; send the link of a channel post.") if len(segments) == 3 and not segments[1].isdigit(): raise LinkError(f"Unsupported Telegram link format. Use a post link such as {EXAMPLE}") kind = "public" message_id = int(message) if message_id <= 0: raise LinkError("The message ID in the link must be a positive number.") return MessageLink(kind=kind, channel=channel, message_id=message_id, url=raw)