✅ Регистрация и верификация жильцов ✅ Рейтинг активности (уровни) ✅ Детект шпионов (Score 0-100) ✅ Анти-мат система (3 предупреждения → бан) ✅ Админ-панель с рассылкой ✅ Учёт квартир (несколько жильцов) ✅ Телефоны экстренных служб и мастеров ✅ Еженедельный экспорт JSON ✅ Прокси (socks5) для обхода РКН Бекап: backups/versions/v1.0_mvp_2026-02-26/ Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
197 lines
7.7 KiB
Python
197 lines
7.7 KiB
Python
"""
|
||
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'],
|
||
}
|