first commit
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user