Spaces:
Paused
Paused
File size: 4,980 Bytes
2e79f82 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | """Find a channel from what a person types: @username, t.me link, numeric id (-100...), or an invite link.
Used by /shift. The bot can only talk to a channel it can see: registered channels (the database has their access
details), channels it has met in updates, public channels by @username, and channels it is a member of (invite link).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Optional
from telethon import functions, types
from .common import Ctx, UserError
_REF_RX = re.compile(r"(?i)^(?:https?://)?(?:t\.me|telegram\.me)/(?:c/(\d+)|([A-Za-z0-9_]{3,32}))(?:/\d+)?/?$")
_INVITE_RX = re.compile(r"(?i)^(?:https?://)?(?:t\.me|telegram\.me|telegram\.dog)/(?:\+|joinchat/)([A-Za-z0-9_-]{6,})/?$")
_TG_INVITE_RX = re.compile(r"(?i)^tg://join\?invite=([A-Za-z0-9_-]{6,})$")
def parse_channel_ref(text: str):
"""@name / t.me link / numeric id -> username (str) or channel id (int); None if unrecognised."""
t = (text or "").strip()
m = _REF_RX.match(t)
if m:
return int(m.group(1)) if m.group(1) else m.group(2)
t = t.lstrip("@")
if re.fullmatch(r"-?\d+", t):
s = str(abs(int(t)))
return int(s[3:]) if s.startswith("100") and len(s) > 10 else int(s)
if re.fullmatch(r"[A-Za-z0-9_]{3,32}", t):
return t
return None
def classify_ref(text: str) -> tuple:
"""-> ("invite", hash) | ("username", name) | ("id", int) | (None, None)"""
t = (text or "").strip()
m = _INVITE_RX.match(t) or _TG_INVITE_RX.match(t)
if m:
return "invite", m.group(1)
ref = parse_channel_ref(t)
if isinstance(ref, int):
return "id", ref
if isinstance(ref, str):
return "username", ref
return None, None
@dataclass
class ChannelRef:
"""A channel as the bot can reach it."""
id: int
access_hash: int
title: str
username: Optional[str]
registered: bool # the bot's database knows it (it can be used with /new, /posts ...)
def _from_entity(ent, registered: bool) -> ChannelRef:
if not isinstance(ent, types.Channel):
raise UserError("That is not a channel (it looks like a user, bot or group). Only channels can be used here.")
if not ent.broadcast:
raise UserError("That is a group, not a channel. Only channels can be used here.")
if not ent.access_hash or getattr(ent, "min", False):
raise UserError(
"I can see that channel, but Telegram has not given me its access details. Use its @username, or add the "
"bot to the channel as an admin and post something there first."
)
return ChannelRef(ent.id, ent.access_hash, ent.title or "", ent.username, registered)
async def _entity(client, what):
try:
return await client.get_entity(what)
except Exception:
return None
async def resolve_channel(ctx: Ctx, text: str) -> ChannelRef:
"""The channel `text` points at, or a UserError that says what to do."""
kind, value = classify_ref(text)
if kind is None:
raise UserError(
f"“{text}” is not a channel I understand. Use an @username, a t.me link, the channel id (-100…) or an invite link."
)
db, client = ctx.db, ctx.client
if kind == "username":
for ch in await db.list_channels(active_only=False):
if ch.username and ch.username.lower() == value.lower():
return ChannelRef(ch.id, ch.access_hash, ch.title, ch.username, ch.active)
ent = await _entity(client, value)
if ent is None:
raise UserError(f"I couldn't find a channel called @{value}. Check the name, or add the bot to it as an admin.")
elif kind == "id":
ch = await db.get_channel(value)
if ch is not None:
return ChannelRef(ch.id, ch.access_hash, ch.title, ch.username, ch.active)
ent = ctx.seen_channels.get(value) or await _entity(client, types.PeerChannel(value))
if ent is None:
raise UserError(
f"I don't know a channel with the id {value}. A bot can only use an id for channels it is part of; "
"use the @username instead, or add the bot to the channel as an admin."
)
else: # invite link: only tells which channel it is; the bot cannot join by itself
try:
info = await client(functions.messages.CheckChatInviteRequest(hash=value))
except Exception as e:
raise UserError(f"That invite link doesn't work ({type(e).__name__}).")
chat = getattr(info, "chat", None)
if chat is None:
raise UserError(
"The bot is not a member of that channel, so it cannot use that invite link. Add the bot to the channel "
"as an admin first (a bot cannot join by itself)."
)
ent = chat
known = await db.get_channel(getattr(ent, "id", 0))
return _from_entity(ent, known is not None and known.active)
|