""" Фильтр проверки подписки пользователя на каналы/группы """ from typing import Union, Optional from dataclasses import dataclass from aiogram import Bot from aiogram.enums import ChatMemberStatus from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError from aiogram.filters import BaseFilter from aiogram.types import Message, CallbackQuery from middleware.loggers import logger __all__ = ('IsSubscribed', 'SubscriptionChecker') @dataclass class ChannelInfo: """Информация о канале для проверки подписки""" id: Union[str, int] name: Optional[str] = None invite_link: Optional[str] = None class SubscriptionChecker: """ Вспомогательный класс для проверки подписок. Может использоваться отдельно от фильтра. """ # Статусы, считающиеся подпиской SUBSCRIBED_STATUSES: set[str] = { ChatMemberStatus.MEMBER, ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.CREATOR } # Статусы, означающие отсутствие подписки NOT_SUBSCRIBED_STATUSES: set[str] = { ChatMemberStatus.LEFT, ChatMemberStatus.KICKED, ChatMemberStatus.RESTRICTED # Опционально } @classmethod async def is_subscribed( cls, bot: Bot, user_id: int, channel_id: Union[str, int] ) -> bool: """ Проверяет подписку одного пользователя на один канал. Args: bot: Экземпляр бота user_id: ID пользователя channel_id: ID или username канала Returns: bool: True если подписан """ try: member = await bot.get_chat_member( chat_id=channel_id, user_id=user_id ) is_sub = member.status in cls.SUBSCRIBED_STATUSES logger.debug( f"Проверка подписки user={user_id} на канал={channel_id}: {member.status} ({'✅' if is_sub else '❌'})", log_type='SUBSCRIPTION' ) return is_sub except TelegramBadRequest as e: logger.warning( f"Канал {channel_id} недоступен или неверный ID: {e}", log_type='SUBSCRIPTION' ) return False except TelegramForbiddenError as e: logger.error( f"Бот не имеет доступа к каналу {channel_id}: {e}", log_type='SUBSCRIPTION' ) return False except Exception as e: logger.error( f"Непредвиденная ошибка проверки подписки на {channel_id}: {e}", log_type='SUBSCRIPTION' ) return False @classmethod async def check_all_channels( cls, bot: Bot, user_id: int, channels: list[Union[str, int]] ) -> dict[Union[str, int], bool]: """ Проверяет подписку на несколько каналов одновременно. Args: bot: Экземпляр бота user_id: ID пользователя channels: Список ID/username каналов Returns: dict: Словарь {channel_id: is_subscribed} """ results = {} for channel in channels: results[channel] = await cls.is_subscribed(bot, user_id, channel) return results @classmethod async def get_not_subscribed_channels( cls, bot: Bot, user_id: int, channels: list[Union[str, int]] ) -> list[Union[str, int]]: """ Возвращает список каналов, на которые пользователь НЕ подписан. Args: bot: Экземпляр бота user_id: ID пользователя channels: Список ID/username каналов Returns: list: Список каналов без подписки """ not_subscribed = [] for channel in channels: if not await cls.is_subscribed(bot, user_id, channel): not_subscribed.append(channel) return not_subscribed class IsSubscribed(BaseFilter): """ Фильтр для проверки подписки пользователя на каналы/группы. Поддерживает: - Публичные каналы (username: "@channel_name") - Приватные каналы/группы (ID: -1001234567890) - Проверку всех или хотя бы одного канала - Работу с Message и CallbackQuery Attributes: channels: Список ID или username каналов для проверки require_all: Требовать подписку на все каналы (True) или хотя бы один (False) Examples: >> # Проверка подписки на один канал >> @router.message(IsSubscribed(["@my_channel"])) >> async def handler(message: Message): ... await message.answer("Ты подписан!") >> # Проверка на несколько каналов (все обязательны) >> @router.message(IsSubscribed(["@channel1", -1001234567890], require_all=True)) >> async def handler(message: Message): ... await message.answer("Ты подписан на все каналы!") >> # Проверка на несколько каналов (хотя бы один) >> @router.message(IsSubscribed(["@channel1", "@channel2"], require_all=False)) >> async def handler(message: Message): ... await message.answer("Ты подписан хотя бы на один канал!") """ def __init__( self, channels: list[Union[str, int]], require_all: bool = True ) -> None: """ Инициализация фильтра. Args: channels: Список ID или username каналов require_all: True = все каналы, False = хотя бы один """ if not channels: raise ValueError("Список каналов не может быть пустым") self.channels = channels self.require_all = require_all async def __call__( self, event: Union[Message, CallbackQuery], bot: Bot ) -> Union[bool, dict]: """ Проверка подписки. Args: event: Message или CallbackQuery bot: Экземпляр бота Returns: bool или dict: True/False для простой проверки, dict с деталями для сложной логики """ user_id = event.from_user.id # Проверка всех каналов results = await SubscriptionChecker.check_all_channels( bot, user_id, self.channels ) # Логика проверки if self.require_all: # Все каналы обязательны is_passed = all(results.values()) else: # Хотя бы один канал is_passed = any(results.values()) # Логирование if not is_passed: not_subscribed = [ch for ch, sub in results.items() if not sub] logger.info( f"Пользователь {user_id} не подписан на: {not_subscribed}", log_type='SUBSCRIPTION', message=event if isinstance(event, Message) else None ) # Возвращаем результат + детали для handler return { 'is_subscribed': is_passed, 'subscription_results': results, 'not_subscribed_channels': [ch for ch, sub in results.items() if not sub] } if not is_passed else is_passed