domovoy_bot/services/rate_limiter.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

197 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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'],
}