File size: 2,768 Bytes
a0261c0
 
 
 
 
 
b37ebd1
 
 
 
 
a0261c0
b37ebd1
 
a0261c0
b37ebd1
 
 
 
a0261c0
 
b37ebd1
 
a0261c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b37ebd1
a0261c0
 
 
 
 
 
 
b37ebd1
a0261c0
 
 
 
 
 
 
 
 
 
 
b37ebd1
a0261c0
 
 
b37ebd1
 
a0261c0
 
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
"""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)