Files
drive/link_parser.py
T
2026-10-18 23:48:22 +05:30

113 lines
4.6 KiB
Python

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