cipher-attacks's picture
2-channel gate, hide empty courses
a0261c0
Raw History Blame Contribute Delete
2.77 kB
"""Force-join gate. The user must be a member of EVERY channel/group in GATE_CHANNELS.
The bot must be an ADMIN of each of them, otherwise Telegram will not tell it who the members are.
A misconfiguration (bot not admin / wrong username) BLOCKS users and alerts the admin loudly,
instead of silently letting everybody in. Temporary network trouble does not punish students."""
import asyncio
import logging
import time
from aiogram import Bot
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from app.config import get_settings
from app.core.notify import notify_admins
log = logging.getLogger(__name__)
_OK = {ChatMemberStatus.CREATOR, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.MEMBER}
_cache: dict[int, float] = {}
_alerted: dict[str, float] = {}
_TTL = 1800 # seconds a positive result is trusted (/start and the "I joined" button always re-check)
def channels() -> list[str]:
return get_settings().gate_list
def channel_url(handle: str) -> str:
return f"https://t.me/{handle.lstrip('@')}"
async def _alert(bot: Bot, handle: str, exc: Exception) -> None:
now = time.monotonic()
if now - _alerted.get(handle, 0) < 600:
return
_alerted[handle] = now
log.error("Gate cannot verify %s: %r", handle, exc)
await notify_admins(
bot,
f"⚠️ The join-gate cannot check {handle}: {type(exc).__name__}: {exc}\n"
f"Make the bot an ADMIN of {handle} and check the username (run /gatecheck). "
"Until it is fixed, users are NOT let through.",
)
async def _is_in(bot: Bot, handle: str, user_id: int) -> bool:
try:
member = await bot.get_chat_member(handle, user_id)
return member.status in _OK
except (TelegramBadRequest, TelegramForbiddenError) as exc: # configuration problem -> stay closed
await _alert(bot, handle, exc)
return False
except Exception as exc: # network hiccup / flood wait: do not punish students
log.warning("Gate check for %s hit a temporary error: %r", handle, exc)
return True
async def missing(bot: Bot, user_id: int, *, fresh: bool = False) -> list[str]:
"""Channels the user still has to join (empty list = allowed in)."""
now = time.time()
if not fresh and _cache.get(user_id, 0) > now:
return []
handles = channels()
results = await asyncio.gather(*[_is_in(bot, h, user_id) for h in handles])
left = [h for h, ok in zip(handles, results) if not ok]
if left:
_cache.pop(user_id, None)
else:
_cache[user_id] = now + _TTL
return left
async def is_member(bot: Bot, user_id: int, *, fresh: bool = False) -> bool:
return not await missing(bot, user_id, fresh=fresh)