diff --git a/bot/__main__.py b/bot/__main__.py index 64fb811..f8b5833 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -31,7 +31,10 @@ async def main() -> None: asyncio.create_task(sweeper_loop(bot=bot, cfg=cfg, db=db)) try: - await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) + allowed_updates = list(dp.resolve_used_update_types()) + if "chat_member" not in allowed_updates: + allowed_updates.append("chat_member") + await dp.start_polling(bot, allowed_updates=allowed_updates) finally: await db.close() diff --git a/bot/handlers.py b/bot/handlers.py index 2691772..ecf5216 100644 --- a/bot/handlers.py +++ b/bot/handlers.py @@ -10,6 +10,7 @@ from aiogram.types import ( CallbackQuery, ChatMemberAdministrator, ChatMemberOwner, + ChatMemberUpdated, InlineKeyboardButton, InlineKeyboardMarkup, Message, @@ -87,6 +88,32 @@ def _challenge_kb(chat_id: int, user_id: int) -> InlineKeyboardMarkup: ) +async def _on_user_joined( + *, + bot: Bot, + cfg: Config, + db: Database, + chat_id: int, + user_id: int, + user_full_name: str, +) -> None: + try: + if await _is_admin(bot, chat_id, user_id): + return + except TelegramBadRequest: + return + + await db.delete_verified(chat_id=chat_id, user_id=user_id) + await _ensure_challenge( + bot=bot, + cfg=cfg, + db=db, + chat_id=chat_id, + user_id=user_id, + user_full_name=user_full_name, + ) + + async def _ensure_challenge( *, bot: Bot, @@ -162,15 +189,7 @@ async def on_new_members(message: Message, bot: Bot, cfg: Config, db: Database) for user in message.new_chat_members: if user.is_bot: continue - try: - if await _is_admin(bot, chat_id, user.id): - continue - except TelegramBadRequest: - continue - - # Force verification on every (re)join: prevents bypass if user was verified earlier. - await db.delete_verified(chat_id=chat_id, user_id=user.id) - await _ensure_challenge( + await _on_user_joined( bot=bot, cfg=cfg, db=db, @@ -180,6 +199,40 @@ async def on_new_members(message: Message, bot: Bot, cfg: Config, db: Database) ) +@router.chat_member() +async def on_chat_member_updated( + event: ChatMemberUpdated, + bot: Bot, + cfg: Config, + db: Database, +) -> None: + """ + Supergroups (especially linked discussion groups) often hide join service messages. + In that case Telegram sends chat_member updates instead of new_chat_members. + """ + chat_id = event.chat.id + if not _is_allowed_chat(cfg, chat_id): + return + + user = event.new_chat_member.user + if user.is_bot: + return + + old_status = event.old_chat_member.status + new_status = event.new_chat_member.status + if old_status not in ("left", "kicked") or new_status not in ("member", "restricted"): + return + + await _on_user_joined( + bot=bot, + cfg=cfg, + db=db, + chat_id=chat_id, + user_id=user.id, + user_full_name=user.full_name, + ) + + @router.callback_query(F.data.startswith("verify:")) async def on_verify( callback: CallbackQuery,