mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
Rate limiter для ограничения частоты запросов пользователей.
|
||||
Обеспечивает защиту от спама и злоупотреблений.
|
||||
|
||||
Особенности:
|
||||
- Sliding window алгоритм для точного учета запросов во времени
|
||||
- Поддержка ролевой системы (администраторы не ограничены)
|
||||
- Дифференцированные лимиты для пользователей разных рангов:
|
||||
* Новые пользователи (Рядовой, Ефрейтор): 5 запросов/минуту
|
||||
* Обычные пользователи: 10 запросов/минуту
|
||||
* Администраторы: без ограничений
|
||||
- Потокобезопасная реализация с автоматической очисткой старых записей
|
||||
- Интеграция с системой рангов для определения статуса пользователя
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Optional, Tuple
|
||||
from collections import defaultdict
|
||||
from .permissions import UserRole
|
||||
|
||||
|
||||
class RateLimitExceeded(Exception):
|
||||
"""Исключение при превышении лимита запросов"""
|
||||
|
||||
def __init__(self, retry_after: int):
|
||||
self.retry_after = retry_after
|
||||
super().__init__(f"Rate limit exceeded. Retry after {retry_after} seconds")
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""
|
||||
Rate limiter на основе sliding window алгоритма.
|
||||
Отслеживает запросы пользователей в скользящем окне времени.
|
||||
|
||||
Основные возможности:
|
||||
- Проверка лимитов без записи запроса (check_limit)
|
||||
- Запись запроса с проверкой лимита (record_request)
|
||||
- Получение количества оставшихся запросов (get_remaining_requests)
|
||||
- Получение статистики использования (get_stats)
|
||||
- Автоматическая очистка устаревших записей
|
||||
- Поддержка ранговой системы для дифференциации лимитов
|
||||
|
||||
Лимиты по умолчанию:
|
||||
- Новые пользователи (Рядовой/Ефрейтор): 5 запросов в минуту
|
||||
- Обычные пользователи: 10 запросов в минуту
|
||||
- Администраторы: без ограничений
|
||||
"""
|
||||
|
||||
def __init__(self, window_size: int = 60, max_requests: int = 10, cleanup_interval: int = 300):
|
||||
"""
|
||||
Инициализация rate limiter.
|
||||
|
||||
Args:
|
||||
window_size: Размер окна в секундах (по умолчанию 60 секунд = 1 минута)
|
||||
max_requests: Максимальное количество запросов в окне (по умолчанию 10)
|
||||
cleanup_interval: Интервал очистки старых записей в секундах (по умолчанию 5 минут)
|
||||
"""
|
||||
self.window_size = window_size
|
||||
self.max_requests = max_requests
|
||||
self.cleanup_interval = cleanup_interval
|
||||
|
||||
# Структура: user_id -> список временных меток запросов
|
||||
self.requests: Dict[int, list] = defaultdict(list)
|
||||
|
||||
# Блокировка для потокобезопасности
|
||||
self.lock = threading.RLock()
|
||||
|
||||
# Настройки для новых пользователей (более строгие ограничения)
|
||||
self.new_user_max_requests = 5 # 5 запросов в минуту для новых пользователей
|
||||
|
||||
# Исключения для ролей (администраторы не ограничены)
|
||||
self.exempt_roles = {UserRole.ADMIN, UserRole.SUPER_ADMIN}
|
||||
|
||||
# Запуск автоматической очистки
|
||||
self._start_cleanup_thread()
|
||||
|
||||
def _start_cleanup_thread(self):
|
||||
"""Запуск фонового потока для очистки старых записей"""
|
||||
def cleanup_worker():
|
||||
while True:
|
||||
time.sleep(self.cleanup_interval)
|
||||
self._cleanup_old_requests()
|
||||
|
||||
thread = threading.Thread(target=cleanup_worker, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _cleanup_old_requests(self):
|
||||
"""Очистка старых временных меток запросов"""
|
||||
current_time = time.time()
|
||||
cutoff_time = current_time - self.window_size
|
||||
|
||||
with self.lock:
|
||||
for user_id in list(self.requests.keys()):
|
||||
# Оставляем только свежие запросы
|
||||
self.requests[user_id] = [
|
||||
timestamp for timestamp in self.requests[user_id]
|
||||
if timestamp > cutoff_time
|
||||
]
|
||||
|
||||
# Удаляем пустые записи
|
||||
if not self.requests[user_id]:
|
||||
del self.requests[user_id]
|
||||
|
||||
def _get_max_requests_for_user(self, user_role: Optional[UserRole], is_new_user: bool = False) -> int:
|
||||
"""
|
||||
Определение максимального количества запросов для пользователя.
|
||||
|
||||
Args:
|
||||
user_role: Роль пользователя
|
||||
is_new_user: Является ли пользователь новым
|
||||
|
||||
Returns:
|
||||
Максимальное количество запросов в окне
|
||||
"""
|
||||
# Администраторы не ограничены
|
||||
if user_role in self.exempt_roles:
|
||||
return float('inf')
|
||||
|
||||
# Новые пользователи имеют более строгие ограничения
|
||||
if is_new_user:
|
||||
return self.new_user_max_requests
|
||||
|
||||
return self.max_requests
|
||||
|
||||
def _is_new_user(self, user_id: int, registration_time: Optional[float] = None, user_rank: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Определение, является ли пользователь новым на основе ранга или количества запросов.
|
||||
|
||||
Логика определения новых пользователей:
|
||||
1. Если указано время регистрации - проверяем, прошло ли менее 24 часов
|
||||
2. Если известен ранг - проверяем, является ли он начальным ("Рядовой", "Ефрейтор")
|
||||
3. Иначе проверяем количество предыдущих запросов (< max_requests)
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
registration_time: Время регистрации (если известно)
|
||||
user_rank: Ранг пользователя (если известен)
|
||||
|
||||
Returns:
|
||||
True если пользователь новый (низкий ранг или мало запросов)
|
||||
"""
|
||||
# Если время регистрации известно, проверяем его
|
||||
if registration_time:
|
||||
return (time.time() - registration_time) < 86400 # 24 часа
|
||||
|
||||
# Если ранг известен, считаем нового пользователями с низкими рангами
|
||||
if user_rank:
|
||||
# Новички: Рядовой и Ефрейтор считаются новыми пользователями
|
||||
low_ranks = ["Рядовой", "Ефрейтор"]
|
||||
return user_rank in low_ranks
|
||||
|
||||
# Иначе проверяем по количеству предыдущих запросов
|
||||
# Если запросов меньше максимального лимита, считаем пользователя новым
|
||||
with self.lock:
|
||||
return len(self.requests.get(user_id, [])) < self.max_requests
|
||||
|
||||
async def check_limit(self, user_id: int, user_role: Optional[UserRole] = None,
|
||||
registration_time: Optional[float] = None, user_rank: Optional[str] = None) -> Tuple[bool, Optional[int]]:
|
||||
"""
|
||||
Проверка, не превышен ли лимит запросов.
|
||||
Только проверяет, не добавляет запрос в счетчик.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
user_role: Роль пользователя
|
||||
registration_time: Время регистрации пользователя
|
||||
user_rank: Ранг пользователя
|
||||
|
||||
Returns:
|
||||
Кортеж (разрешено: bool, время_ожидания: Optional[int])
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
with self.lock:
|
||||
# Определяем лимиты для пользователя
|
||||
is_new_user = self._is_new_user(user_id, registration_time, user_rank)
|
||||
max_requests = self._get_max_requests_for_user(user_role, is_new_user)
|
||||
|
||||
# Администраторы всегда разрешены
|
||||
if max_requests == float('inf'):
|
||||
return True, None
|
||||
|
||||
# Очищаем старые запросы для этого пользователя
|
||||
cutoff_time = current_time - self.window_size
|
||||
user_requests = self.requests[user_id]
|
||||
user_requests[:] = [t for t in user_requests if t > cutoff_time]
|
||||
|
||||
# Проверяем лимит
|
||||
if len(user_requests) >= max_requests:
|
||||
# Вычисляем время до следующего разрешенного запроса
|
||||
oldest_request = min(user_requests)
|
||||
retry_after = int(self.window_size - (current_time - oldest_request))
|
||||
return False, max(1, retry_after)
|
||||
|
||||
# Возвращаем разрешение без добавления запроса
|
||||
return True, None
|
||||
|
||||
async def record_request(self, user_id: int, user_role: Optional[UserRole] = None,
|
||||
registration_time: Optional[float] = None, user_rank: Optional[str] = None) -> None:
|
||||
"""
|
||||
Запись факта выполнения запроса (для случаев, когда проверка уже пройдена).
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
user_role: Роль пользователя
|
||||
registration_time: Время регистрации пользователя
|
||||
user_rank: Ранг пользователя
|
||||
"""
|
||||
current_time = time.time()
|
||||
|
||||
with self.lock:
|
||||
# Определяем лимиты для пользователя
|
||||
is_new_user = self._is_new_user(user_id, registration_time, user_rank)
|
||||
max_requests = self._get_max_requests_for_user(user_role, is_new_user)
|
||||
|
||||
# Администраторы всегда разрешены
|
||||
if max_requests == float('inf'):
|
||||
return
|
||||
|
||||
# Очищаем старые запросы для этого пользователя
|
||||
cutoff_time = current_time - self.window_size
|
||||
user_requests = self.requests[user_id]
|
||||
user_requests[:] = [t for t in user_requests if t > cutoff_time]
|
||||
|
||||
# Проверяем лимит
|
||||
if len(user_requests) >= max_requests:
|
||||
# Вычисляем время до следующего разрешенного запроса
|
||||
oldest_request = min(user_requests)
|
||||
retry_after = int(self.window_size - (current_time - oldest_request))
|
||||
raise RateLimitExceeded(max(1, retry_after))
|
||||
|
||||
# Добавляем текущий запрос
|
||||
user_requests.append(current_time)
|
||||
|
||||
def get_remaining_requests(self, user_id: int, user_role: Optional[UserRole] = None,
|
||||
registration_time: Optional[float] = None, user_rank: Optional[str] = None) -> int:
|
||||
"""
|
||||
Получение количества оставшихся запросов в текущем окне.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
user_role: Роль пользователя
|
||||
registration_time: Время регистрации пользователя
|
||||
user_rank: Ранг пользователя
|
||||
|
||||
Returns:
|
||||
Количество оставшихся запросов
|
||||
"""
|
||||
with self.lock:
|
||||
is_new_user = self._is_new_user(user_id, registration_time, user_rank)
|
||||
max_requests = self._get_max_requests_for_user(user_role, is_new_user)
|
||||
|
||||
if max_requests == float('inf'):
|
||||
return 999 # Для администраторов возвращаем большое число
|
||||
|
||||
# Очищаем старые запросы перед подсчетом
|
||||
current_time = time.time()
|
||||
cutoff_time = current_time - self.window_size
|
||||
user_requests = self.requests[user_id]
|
||||
user_requests[:] = [t for t in user_requests if t > cutoff_time]
|
||||
|
||||
current_requests = len(user_requests)
|
||||
return max(0, max_requests - current_requests)
|
||||
|
||||
def get_stats(self) -> Dict[str, any]:
|
||||
"""
|
||||
Получение статистики rate limiter.
|
||||
|
||||
Returns:
|
||||
Словарь со статистикой
|
||||
"""
|
||||
with self.lock:
|
||||
total_users = len(self.requests)
|
||||
total_requests = sum(len(requests) for requests in self.requests.values())
|
||||
|
||||
return {
|
||||
'total_users_tracked': total_users,
|
||||
'total_requests_in_window': total_requests,
|
||||
'window_size_seconds': self.window_size,
|
||||
'default_max_requests': self.max_requests,
|
||||
'new_user_max_requests': self.new_user_max_requests
|
||||
}
|
||||
|
||||
|
||||
# Глобальный экземпляр rate limiter
|
||||
# Настройки согласно требованиям:
|
||||
# - 10 команд/минуту для обычных пользователей
|
||||
# - 5 команд/минуту для новых пользователей (Рядовой, Ефрейтор)
|
||||
# - Без ограничений для администраторов
|
||||
rate_limiter = RateLimiter(window_size=60, max_requests=10)
|
||||
Reference in New Issue
Block a user