""" Rate Limiter — ограничение частоты сообщений (анти-спам) """ import logging from datetime import datetime, timedelta from typing import Dict, List, Tuple, Optional from sqlalchemy import select, func, delete from sqlalchemy.ext.asyncio import AsyncSession logger = logging.getLogger(__name__) class RateLimiter: """Сервис ограничения частоты сообщений""" def __init__(self, session: AsyncSession): self.session = session self.config = self._get_default_config() def _get_default_config(self) -> Dict: """Конфигурация по умолчанию""" return { 'max_messages': 10, # Максимум сообщений 'period_seconds': 60, # За период (секунд) 'mute_duration_minutes': 30, # Длительность мута 'warnings_before_mute': 2, # Предупреждения перед мутом 'exclude_admins': True, # Исключить админов 'exclude_verified': False, # Исключить верифицированных } async def check_rate_limit(self, user_id: int) -> Tuple[bool, Optional[int]]: """ Проверить лимит сообщений для пользователя Returns: (is_limited, messages_count) — превышен ли лимит и текущее кол-во """ from database.models import MessageRate # Получаем временное окно period_start = datetime.utcnow() - timedelta(seconds=self.config['period_seconds']) # Считаем сообщения за период stmt = ( select(func.count(MessageRate.id)) .where(MessageRate.user_id == user_id) .where(MessageRate.timestamp >= period_start) ) result = await self.session.execute(stmt) count = result.scalar() or 0 is_limited = count >= self.config['max_messages'] return is_limited, count async def add_message(self, user_id: int): """Добавить сообщение в счётчик""" from database.models import MessageRate message_rate = MessageRate(user_id=user_id) self.session.add(message_rate) # Чистим старые записи (старше периода) period_start = datetime.utcnow() - timedelta(seconds=self.config['period_seconds'] * 2) await self.session.execute( delete(MessageRate).where(MessageRate.timestamp < period_start) ) async def get_user_stats(self, user_id: int) -> Dict: """Получить статистику пользователя""" from database.models import MessageRate, User # Период period_start = datetime.utcnow() - timedelta(seconds=self.config['period_seconds']) # Сообщения за период stmt = ( select(func.count(MessageRate.id)) .where(MessageRate.user_id == user_id) .where(MessageRate.timestamp >= period_start) ) result = await self.session.execute(stmt) messages_count = result.scalar() or 0 # Получаем пользователя user = await self.session.get(User, user_id) return { 'messages_count': messages_count, 'max_messages': self.config['max_messages'], 'period_seconds': self.config['period_seconds'], 'is_muted': user.is_muted if user else False, 'mute_until': user.mute_until.isoformat() if user and user.mute_until else None, 'rate_limit_warnings': user.rate_limit_warnings if user else 0, } async def get_all_violators(self) -> List[Dict]: """Получить всех нарушителей за последний период""" from database.models import MessageRate, User period_start = datetime.utcnow() - timedelta(seconds=self.config['period_seconds']) stmt = ( select( MessageRate.user_id, func.count(MessageRate.id).label('msg_count') ) .where(MessageRate.timestamp >= period_start) .group_by(MessageRate.user_id) .having(func.count(MessageRate.id) > self.config['max_messages'] / 2) .order_by(func.count(MessageRate.id).desc()) .limit(20) ) result = await self.session.execute(stmt) violators = [] for row in result.all(): user = await self.session.get(User, row.user_id) violators.append({ 'user_id': row.user_id, 'username': user.username if user else None, 'full_name': user.full_name if user else None, 'apartment': user.apartment if user else None, 'messages_count': row.msg_count, 'is_muted': user.is_muted if user else False, }) return violators def get_config(self) -> Dict: """Получить конфигурацию""" return self.config def update_config(self, key: str, value): """Обновить конфигурацию""" if key in self.config: self.config[key] = value logger.info(f'RateLimiter config updated: {key}={value}') async def mute_user(self, user_id: int, duration_minutes: int = None) -> datetime: """Замутить пользователя""" from database.models import User if duration_minutes is None: duration_minutes = self.config['mute_duration_minutes'] user = await self.session.get(User, user_id) if user: user.is_muted = True user.mute_until = datetime.utcnow() + timedelta(minutes=duration_minutes) await self.session.commit() mute_until = user.mute_until logger.info(f'Пользователь {user_id} замучен до {mute_until}') return mute_until return None async def unmute_user(self, user_id: int) -> bool: """Размутить пользователя""" from database.models import User user = await self.session.get(User, user_id) if user: user.is_muted = False user.mute_until = None await self.session.commit() logger.info(f'Пользователь {user_id} размучен') return True return False async def check_mute_expired(self, user_id: int) -> bool: """Проверить истёк ли мут""" from database.models import User user = await self.session.get(User, user_id) if user and user.is_muted and user.mute_until: if datetime.utcnow() >= user.mute_until: # Мут истёк — размучиваем user.is_muted = False user.mute_until = None await self.session.commit() logger.info(f'Мут пользователя {user_id} истёк') return True return False def get_stats(self) -> Dict: """Получить статистику""" return { 'max_messages': self.config['max_messages'], 'period_seconds': self.config['period_seconds'], 'mute_duration_minutes': self.config['mute_duration_minutes'], 'warnings_before_mute': self.config['warnings_before_mute'], }