This commit is contained in:
2026-02-20 03:12:47 +07:00
parent 5d350d0885
commit 5aca4e8438
23 changed files with 2291 additions and 1330 deletions

View File

@@ -1,19 +1,13 @@
"""
Middleware для проверки сообщений на запрещённые слова (банворды).
Pipeline проверки:
1. Пропускаем админов и служебные сообщения
2. Проверяем whitelist (исключения)
3. Проверяем режим silence (удаляем всё)
4. Проверяем режим conflict (конфликтные слова)
5. Проверяем постоянные банворды (substring, lemma, part)
6. Проверяем временные банворды
7. Если найдено - удаляем, логируем, уведомляем админов
НОВОЕ: Все проверки работают с нормализацией повторяющихся букв (3+ → 1).
...
✅ ИСПРАВЛЕНО:
- ❌ PatternError: bad character range 🀀-\\\\ (исправлено экранирование Unicode)
- ✅ НЕТ уведомлений в режиме тишины
"""
from typing import Callable, Dict, Any, Awaitable, Optional
import re
import unicodedata
from aiogram import BaseMiddleware
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
@@ -28,263 +22,162 @@ __all__ = ("BanWordsMiddleware",)
class BanWordsMiddleware(BaseMiddleware):
"""
Middleware для фильтрации сообщений с банвордами.
Проверяет каждое текстовое сообщение на наличие запрещённых слов,
удаляет спам и уведомляет администраторов.
"""
def __init__(self):
"""Инициализирует middleware"""
super().__init__()
self.manager = get_manager()
async def __call__(
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Message,
data: Dict[str, Any]
self,
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
event: Message,
data: Dict[str, Any]
) -> Any:
"""
Обрабатывает входящие сообщения.
Args:
handler: Следующий обработчик в цепочке
event: Сообщение от пользователя
data: Данные из диспетчера
Returns:
Any: Результат обработчика или None (если сообщение удалено)
"""
# Пропускаем не-текстовые сообщения
if not event.text and not event.caption:
return await handler(event, data)
# Получаем текст (из text или caption)
message_text = event.text or event.caption
# Пропускаем команды (начинаются с /)
if message_text.startswith('/'):
return await handler(event, data)
# Проверяем, является ли пользователь админом
# Админ проверка
user_id = event.from_user.id
is_super_admin = user_id in settings.OWNER_ID
is_admin = is_super_admin or self.manager.is_admin_cached(user_id)
# Админы пропускаются
if is_admin:
return await handler(event, data)
# Проверяем сообщение на банворды
spam_result = await self._check_message(message_text)
if spam_result:
# Найден спам - удаляем и уведомляем
await self._handle_spam(event, spam_result)
return None # Не продолжаем обработку
return None
# Сообщение чистое - пропускаем дальше
return await handler(event, data)
@staticmethod
def _normalize_for_part_check(text: str) -> str:
"""
Нормализует текст для проверки частей слов.
Удаляет ВСЕ символы кроме букв и цифр, приводит к нижнему регистру.
Args:
text: Исходный текст
Returns:
str: Нормализованный текст (только буквы и цифры, нижний регистр)
Examples:
"@Astrixkeepbot" -> "astrixkeepbot"
"hello@world.com" -> "helloworldcom"
"test_123-456" -> "test123456"
"""
return re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9]', '', text.lower())
def _normalize_universal(text: str, mode: str = "strict") -> str:
"""✅ ИСПРАВЛЕНО: Универсальная нормализация для всех типов проверок"""
# БЕЗОПАСНАЯ нормализация - убираем все проблемные символы
text = unicodedata.normalize('NFKC', text)
if mode == "strict": # PART - сохраняем буквы, цифры, пробелы
# ✅ ИСПРАВЛЕНО: безопасный паттерн только для букв/цифр/пробелов
text = re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
else: # SUBSTRING/LEMMA - только буквы и цифры
text = re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9]', '', text)
return BanWordsMiddleware._normalize_repeated_chars(text)
@staticmethod
def _normalize_repeated_chars(text: str, max_repeats: int = 1) -> str:
"""
Убирает повторяющиеся буквы (обход "лееейн" -> "лейн", "телееелооог" -> "телелог").
def _normalize_repeated_chars(text: str) -> str:
"""Убирает повторения >2 (лееееин → лейн)"""
return re.sub(r'([а-яёa-z])\\1{2,}', r'\\1\\1', text, flags=re.IGNORECASE)
Args:
text: Исходное слово
max_repeats: Максимальное количество повторов одной буквы (1 = убрать все повторы)
Returns:
str: Нормализованное слово
Examples:
("лееейн", 1) -> "лейн"
("телееелооог", 1) -> "телелог"
("хеееелооооу", 1) -> "хелоу"
("аааааа", 1) -> "а"
("привеееет", 2) -> "приввеет" (если max_repeats=2)
"""
if max_repeats == 1:
# Заменяем 2+ одинаковых букв подряд на 1 такую же букву
return re.sub(r'([а-яёa-z])\1+', r'\1', text, flags=re.IGNORECASE)
else:
# Заменяем (max_repeats+1)+ одинаковых букв на max_repeats таких букв
pattern = f'([а-яёa-z])\\1{{{max_repeats},}}'
replacement = '\\1' * max_repeats
return re.sub(pattern, replacement, text, flags=re.IGNORECASE)
def _check_repeated_chars(self, text: str) -> Optional[Dict[str, str]]:
"""🔥 Блокирует 3+ повторяющиеся символы подряд"""
# ✅ ИСПРАВЛЕНО: безопасный паттерн только для букв
pattern = r'([а-яёa-zA-Z])\\1{2,}'
matches = re.finditer(pattern, text, flags=re.IGNORECASE)
for match in matches:
char = match.group(1)
count = len(match.group(0))
if count >= 3:
logger.info(f"🔥 ПОВТОРЫ: '{match.group(0)}' ({count}x)", log_type="BANWORDS")
return {"word": f"'{match.group(0)}' ({count}x)", "type": "repeated_chars"}
return None
async def _check_message(self, text: str) -> Optional[Dict[str, str]]:
"""
Проверяет сообщение на наличие банвордов.
Args:
text: Текст сообщения
Returns:
Optional[Dict]: {"word": "найденное_слово", "type": "тип_проверки"} или None
"""
# Нормализуем текст для проверки
text_lower = text.lower()
# 🔥 1. Повторяющиеся символы (лееееин)
repeat_result = self._check_repeated_chars(text_lower)
if repeat_result:
return repeat_result
# 2. ✅ БЕЗОПАСНАЯ нормализация
text_universal = self._normalize_universal(text_lower, "strict") # PART
text_loose = self._normalize_universal(text_lower) # SUBSTRING/LEMMA
text_processed = process_text(text_lower)
# Дополнительно нормализуем повторяющиеся буквы для всех проверок
text_normalized = self._normalize_repeated_chars(text_processed, max_repeats=1)
logger.debug(
f"Проверка текста: исходный='{text[:50]}', обработанный='{text_processed[:50]}', "
f"нормализованный='{text_normalized[:50]}'",
f"🔍 | universal='{text_universal}' | loose='{text_loose}' | proc='{text_processed}'",
log_type="BANWORDS"
)
# === 1. WHITELIST (исключения) ===
# Проверяем оба варианта: с повторами и без
if self.manager.is_whitelisted(text_processed) or self.manager.is_whitelisted(text_normalized):
logger.debug(
f"Сообщение содержит whitelist слово",
log_type="BANWORDS"
)
# 3. WHITELIST
if (self.manager.is_whitelisted(text_processed) or
self.manager.is_whitelisted(text_loose) or
self.manager.is_whitelisted(text_universal)):
return None
# === 2. SILENCE MODE (удаляем всё) ===
# 4. SILENCE MODE
if await self.manager.is_silence_active():
return {
"word": "[режим тишины]",
"type": "silence"
}
return {"word": "[режим тишины]", "type": "silence"}
# === 3. CONFLICT MODE (конфликтные слова) ===
# 5. CONFLICT MODE
if await self.manager.is_conflict_active():
# Проверяем конфликтные подстроки (с нормализацией)
conflict_substring = self.manager.get_banwords_cached(
BanWordType.CONFLICT_SUBSTRING
)
for word in conflict_substring:
word_normalized = self._normalize_repeated_chars(word, max_repeats=1)
if word_normalized in text_normalized:
for word in self.manager.get_banwords_cached(BanWordType.CONFLICT_SUBSTRING):
word_norm = self._normalize_universal(word.lower(), "loose")
if word_norm in text_loose:
return {"word": word, "type": "conflict_substring"}
# Проверяем конфликтные леммы
conflict_lemma = self.manager.get_banwords_cached(
BanWordType.CONFLICT_LEMMA
)
words_in_text = extract_words(text_processed)
for word_text in words_in_text:
word_normalized = self._normalize_repeated_chars(word_text, max_repeats=1)
lemma = get_lemma(word_normalized)
conflict_lemma = self.manager.get_banwords_cached(BanWordType.CONFLICT_LEMMA)
for word_text in extract_words(text_processed):
lemma = get_lemma(self._normalize_repeated_chars(word_text))
if lemma in conflict_lemma:
return {"word": lemma, "type": "conflict_lemma"}
# === 4. SUBSTRING (подстроки) с нормализацией ===
substring_words = self.manager.get_banwords_cached(BanWordType.SUBSTRING)
for word in substring_words:
# Нормализуем и банворд, и текст
word_normalized = self._normalize_repeated_chars(word, max_repeats=1)
if word_normalized in text_normalized:
logger.info(
f"Найдена подстрока: '{word}' (норм: '{word_normalized}') в '{text_normalized[:100]}'",
log_type="BANWORDS"
)
# 6. SUBSTRING
for word in self.manager.get_banwords_cached(BanWordType.SUBSTRING):
word_norm = self._normalize_universal(word.lower())
if word_norm in text_loose:
logger.info(f"✅ SUBSTRING: '{word}''{text_loose}'", log_type="BANWORDS")
return {"word": word, "type": "substring"}
# === 5. PART (части слов без пробелов и спецсимволов) ===
part_words = self.manager.get_banwords_cached(BanWordType.PART)
if part_words:
# Специальная нормализация для PART: удаляем ВСЁ кроме букв и цифр
text_part_normalized = self._normalize_for_part_check(text)
text_part_normalized = self._normalize_repeated_chars(text_part_normalized, max_repeats=1)
# 7. PART (строгая нормализация)
for part in self.manager.get_banwords_cached(BanWordType.PART):
part_norm = self._normalize_universal(part.lower(), "strict")
if part_norm in text_universal:
logger.info(f"✅ PART: '{part}''{text_universal}'", log_type="BANWORDS")
return {"word": part, "type": "part"}
for part in part_words:
part_normalized = self._normalize_for_part_check(part)
part_normalized = self._normalize_repeated_chars(part_normalized, max_repeats=1)
# 8. LEMMA
for word_text in extract_words(text_processed):
lemma = get_lemma(self._normalize_repeated_chars(word_text))
if lemma in self.manager.get_banwords_cached(BanWordType.LEMMA):
logger.info(f"✅ LEMMA: '{lemma}' из '{word_text}'", log_type="BANWORDS")
return {"word": lemma, "type": "lemma"}
if part_normalized in text_part_normalized:
logger.info(
f"Найдена запрещенная часть: '{part}' (норм: '{part_normalized}') "
f"в '{text_part_normalized[:100]}'",
log_type="BANWORDS"
)
return {"word": part, "type": "part"}
# === 6. LEMMA (нормальные формы слов) ===
lemma_words = self.manager.get_banwords_cached(BanWordType.LEMMA)
if lemma_words:
words_in_text = extract_words(text_processed)
for word_text in words_in_text:
# Убираем повторяющиеся буквы ПЕРЕД лемматизацией
word_normalized = self._normalize_repeated_chars(word_text, max_repeats=1)
lemma = get_lemma(word_normalized)
if lemma in lemma_words:
logger.info(
f"Найдена лемма: '{lemma}' из слова '{word_text}' (норм: '{word_normalized}')",
log_type="BANWORDS"
)
return {"word": lemma, "type": "lemma"}
# Банворды не найдены
return None
async def _handle_spam(
self,
message: Message,
spam_result: Dict[str, str]
) -> None:
"""
Обрабатывает найденный спам: удаляет, логирует, уведомляет.
Args:
message: Сообщение со спамом
spam_result: Результат проверки (слово + тип)
"""
async def _handle_spam(self, message: Message, spam_result: Dict[str, str]) -> None:
user = message.from_user
matched_word = spam_result["word"]
match_type = spam_result["type"]
# Получаем текст сообщения
message_text = message.text or message.caption or "[нет текста]"
# === 1. УДАЛЯЕМ СООБЩЕНИЕ ===
try:
await message.delete()
logger.info(
f"Удалено сообщение от @{user.username or user.id} "
f"(слово: '{matched_word}', тип: {match_type})",
log_type="BANWORDS",
user=f"@{user.username}" if user.username else f"id{user.id}"
)
except TelegramBadRequest as e:
logger.error(
f"Не удалось удалить сообщение: {e}",
log_type="BANWORDS",
user=f"@{user.username}" if user.username else f"id{user.id}"
)
# ✅ ПРОВЕРКА: НЕ отправляем уведомления в режиме тишины
if match_type == "silence":
# Удаляем сообщение молча
try:
await message.delete()
logger.info(f"🔇 SILENCE: @{user.username or user.id} удалено молча",
log_type="BANWORDS")
except TelegramBadRequest as e:
logger.error(f"Не удалено (silence): {e}", log_type="BANWORDS")
return
# === 2. ЛОГИРУЕМ В БД ===
# Удаляем
try:
await message.delete()
logger.info(f"🚫 @{user.username or user.id}: '{matched_word}' ({match_type})",
log_type="BANWORDS")
except TelegramBadRequest as e:
logger.error(f"Не удалено: {e}", log_type="BANWORDS")
return
# Логируем в БД (только НЕ silence)
await self.manager.log_spam(
user_id=user.id,
username=user.username or f"id{user.id}",
@@ -294,96 +187,71 @@ class BanWordsMiddleware(BaseMiddleware):
match_type=match_type
)
# === 3. УВЕДОМЛЯЕМ АДМИНОВ ===
# Уведомляем админов (только НЕ silence)
await self._notify_admins(message, matched_word, match_type, message_text)
# Остальные методы без изменений...
async def _notify_admins(
self,
message: Message,
matched_word: str,
match_type: str,
message_text: str
self,
message: Message,
matched_word: str,
match_type: str,
message_text: str
) -> None:
"""
Отправляет уведомление в админский чат с кнопками.
Args:
message: Удалённое сообщение
matched_word: Слово, по которому сработал фильтр
match_type: Тип проверки
message_text: Текст сообщения
"""
user = message.from_user
username = f"@{user.username}" if user.username else f"ID: {user.id}"
# Получаем количество предыдущих нарушений
spam_count = await self.manager.get_user_spam_count(user.id)
chat_title = message.chat.title or "Без названия"
source_thread_id = message.message_thread_id
# Формируем текст уведомления
notification_text = (
f"🚫 <b>Удалено сообщение</b>\n\n"
f"👤 <b>Пользователь:</b> {username}\n"
f"🆔 <b>ID:</b> <code>{user.id}</code>\n"
f"📊 <b>Нарушений:</b> {spam_count}\n\n"
f"🔍 <b>Триггер:</b> <code>{matched_word}</code>\n"
f"📝 <b>Тип:</b> {self._get_type_emoji(match_type)} {match_type}\n\n"
f"💬 <b>Текст:</b>\n"
f"<code>{self._escape_html(message_text[:500])}</code>"
f"🚫 <b>Удалено сообщение</b>\\n\\n"
f"👤 <b>Пользователь:</b> {username}\\n"
f"🆔 <b>ID:</b> <code>{user.id}</code>\\n"
f"📊 <b>Нарушений:</b> {spam_count}\\n\\n"
f"💬 <b>Чат:</b> {self._escape_html(chat_title)}\\n"
f"🆔 <b>Chat ID:</b> <code>{message.chat.id}</code>\\n"
f"{'📌 <b>Topic ID:</b> <code>{source_thread_id}</code>\\n' if source_thread_id else ''}"
f"🔗 <b>Message ID:</b> <code>{message.message_id}</code>\\n\\n"
f"🔍 <b>Триггер:</b> <code>{self._escape_html(matched_word)}</code>\\n"
f"📝 <b>Тип:</b> {self._get_type_emoji(match_type)} {self._escape_html(match_type)}\\n\\n"
f"💬 <b>Текст:</b>\\n<code>{self._escape_html(message_text[:500])}</code>"
)
# Создаём клавиатуру с действиями
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🔨 Забанить",
callback_data=f"spam_ban:{user.id}:{message.chat.id}"
),
InlineKeyboardButton(
text="✅ Закрыть",
callback_data="spam_close"
)
InlineKeyboardButton(text="🔨 Забанить", callback_data=f"spam_ban:{user.id}:{message.chat.id}"),
InlineKeyboardButton(text=" Закрыть", callback_data="spam_close")
],
[
InlineKeyboardButton(
text="📊 Статистика",
callback_data=f"spam_stats:{user.id}"
)
]
[InlineKeyboardButton(text="📊 Статистика", callback_data=f"spam_stats:{user.id}")]
])
# Отправляем уведомление
try:
bot = message.bot
await bot.send_message(
chat_id=settings.ADMIN_CHAT_ID,
admin_chat_id = getattr(settings, "ADMIN_CHAT_ID", None)
admin_thread_id = getattr(settings, "ADMIN_THREAD_ID", None) or None
await message.bot.send_message(
chat_id=admin_chat_id,
text=notification_text,
reply_markup=keyboard,
parse_mode="HTML"
parse_mode="HTML",
message_thread_id=admin_thread_id
)
except Exception as e:
logger.error(
f"Ошибка отправки уведомления админам: {e}",
log_type="BANWORDS"
)
logger.error(f"❌ Уведомление админам: {e}", log_type="BANWORDS")
@staticmethod
def _get_type_emoji(match_type: str) -> str:
"""Возвращает эмодзи для типа проверки"""
emoji_map = {
return {
"substring": "🔤",
"lemma": "📖",
"part": "🧩",
"silence": "🔇",
"conflict_substring": "⚔️",
"conflict_lemma": "⚔️"
}
return emoji_map.get(match_type, "")
"conflict_lemma": "⚔️",
"repeated_chars": "🔁"
}.get(match_type, "")
@staticmethod
def _escape_html(text: str) -> str:
"""Экранирует HTML символы для безопасного отображения"""
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

View File

@@ -68,7 +68,6 @@ class UserSpamStats:
"""Удаляет старые запросы за пределами временного окна"""
cutoff_time = current_time - time_window
# Удаляем старые запросы
new_times = []
new_contexts = []
@@ -121,7 +120,6 @@ class UserSpamStats:
current_time = time()
# 1. КРИТИЧНО: Экстремально быстрая отправка (флуд-бот)
# Если 5+ сообщений за 2 секунды => мгновенный мут
very_recent = [ctx for ctx in recent_contexts if (current_time - ctx.timestamp) < 2.0]
if len(very_recent) >= 5:
return {
@@ -130,7 +128,7 @@ class UserSpamStats:
'severity': 1.0,
'details': f"⚡ Экстремальный флуд: {len(very_recent)} сообщений за 2 секунды",
'instant_block': True,
'block_duration': 600.0 # 10 минут сразу
'block_duration': 600.0
}
# 2. КРИТИЧНО: 8+ сообщений за 5 секунд => агрессивный флуд
@@ -142,13 +140,12 @@ class UserSpamStats:
'severity': 0.95,
'details': f"🔥 Агрессивный флуд: {len(recent_5s)} сообщений за 5 секунд",
'instant_block': True,
'block_duration': 300.0 # 5 минут
'block_duration': 300.0
}
# 3. Медиа-флуд (стикеры/фото/видео)
# 3. Медиа-флуд
media_contexts = [ctx for ctx in recent_contexts if ctx.media_type]
if len(media_contexts) >= 7:
# Проверяем скорость отправки медиа
media_recent = [ctx for ctx in media_contexts if (current_time - ctx.timestamp) < 5.0]
if len(media_recent) >= 6:
return {
@@ -157,7 +154,7 @@ class UserSpamStats:
'severity': 0.9,
'details': f"📸 Медиа-флуд: {len(media_recent)} файлов за 5 секунд",
'instant_block': True,
'block_duration': 240.0 # 4 минуты
'block_duration': 240.0
}
return {
@@ -173,14 +170,14 @@ class UserSpamStats:
text_counts = Counter(texts)
most_common_text, count = text_counts.most_common(1)[0]
if count >= 5: # 5 одинаковых сообщений
if count >= 5:
return {
'is_spam': True,
'reason': 'identical_messages',
'severity': 0.85,
'details': f"📋 Повтор: '{most_common_text[:40]}...' ({count}x)",
'instant_block': True,
'block_duration': 180.0 # 3 минуты
'block_duration': 180.0
}
# 5. Проверка спама callback кнопок
@@ -189,14 +186,14 @@ class UserSpamStats:
callback_counts = Counter(callbacks)
most_common_callback, count = callback_counts.most_common(1)[0]
if count >= 10: # 10 нажатий одной кнопки
if count >= 10:
return {
'is_spam': True,
'reason': 'callback_spam',
'severity': 0.8,
'details': f"🔘 Спам кнопки: {count} нажатий",
'instant_block': True,
'block_duration': 120.0 # 2 минуты
'block_duration': 120.0
}
return {'is_spam': False, 'reason': None, 'severity': 0.0}
@@ -269,11 +266,11 @@ class AntiSpamMiddleware(BaseMiddleware):
- Детекция скорости отправки сообщений
- Адаптивная длительность блокировки
- Различает типы активности
- Бот никогда не банит сам себя
"""
def __init__(
self,
# Базовые лимиты (мягкие, для накопления варнингов)
rate_limit_text: int = 8,
rate_limit_forward: int = 20,
rate_limit_callback: int = 12,
@@ -281,12 +278,10 @@ class AntiSpamMiddleware(BaseMiddleware):
time_window: float = 10.0,
# Предупреждения (уже не так важны — флуд блокируется мгновенно)
warning_limit: int = 3,
base_block_duration: float = 120.0, # 2 минуты за накопленные варнинги
base_block_duration: float = 120.0,
max_block_duration: float = 3600.0,
# Опции
whitelist_admins: bool = True,
progressive_blocking: bool = True,
enable_smart_detection: bool = True,
@@ -318,7 +313,6 @@ class AntiSpamMiddleware(BaseMiddleware):
context.is_reply = event.reply_to_message is not None
context.is_command = bool(context.text and context.text.startswith('/'))
# Определяем тип медиа
if event.photo:
context.media_type = 'photo'
elif event.video:
@@ -350,7 +344,6 @@ class AntiSpamMiddleware(BaseMiddleware):
else:
base_limit = self.rate_limit_text
# Применяем репутацию
if self.enable_reputation:
base_limit = int(base_limit * user_stats.reputation)
@@ -392,6 +385,11 @@ class AntiSpamMiddleware(BaseMiddleware):
if user_id is None:
return await handler(event, data)
# ✅ ИСПРАВЛЕНИЕ: пропускаем самого бота (предотвращает самобан)
bot = data.get("bot")
if bot and user_id == bot.id:
return await handler(event, data)
user_str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
# Whitelist для администраторов
@@ -414,7 +412,7 @@ class AntiSpamMiddleware(BaseMiddleware):
user=user_str
)
# НЕ отправляем сообщение каждый раз — только callback answer
# Только для callback — отвечаем алертом, для сообщений молчим
if isinstance(event, CallbackQuery):
await event.answer(
f"🚫 Блокировка: {self._format_duration(remaining)}",
@@ -426,10 +424,10 @@ class AntiSpamMiddleware(BaseMiddleware):
# Извлекаем контекст сообщения
context = self._extract_context(event)
# Добавляем запрос СНАЧАЛА (важно для детекции скорости)
# Добавляем запрос СНАЧАЛА важно для детекции скорости флуда
user_stats.add_request(current_time, context)
# Очищаем старые запросы
# Очищаем старые запросы за пределами временного окна
user_stats.clean_old_requests(current_time, self.time_window)
# ========== КРИТИЧНО: МГНОВЕННАЯ ДЕТЕКЦИЯ ФЛУДА ==========
@@ -437,10 +435,9 @@ class AntiSpamMiddleware(BaseMiddleware):
spam_analysis = user_stats.detect_spam_patterns(self.time_window)
if spam_analysis.get('is_spam') and spam_analysis.get('instant_block'):
# МГНОВЕННАЯ БЛОКИРОВКА
block_duration = spam_analysis.get('block_duration', 300.0)
user_stats.block(current_time, block_duration)
user_stats.warnings = self.warning_limit # Максимум варнингов
user_stats.warnings = self.warning_limit
spam_stats.instant_blocks += 1
logger.error(
@@ -461,7 +458,7 @@ class AntiSpamMiddleware(BaseMiddleware):
if isinstance(event, Message):
try:
await event.answer(block_message, parse_mode="HTML")
except:
except Exception:
pass
elif isinstance(event, CallbackQuery):
await event.answer(
@@ -471,10 +468,9 @@ class AntiSpamMiddleware(BaseMiddleware):
return None
# ========== ОБЫЧНАЯ ПРОВЕРКА ЛИМИТОВ (для мягких превышений) ==========
# ========== ОБЫЧНАЯ ПРОВЕРКА ЛИМИТОВ ==========
effective_limit = self._get_effective_rate_limit(user_stats, context)
# Подсчитываем релевантные запросы
relevant_requests = 0
for req_context in user_stats.message_contexts:
if context.is_forward and req_context.is_forward:
@@ -493,7 +489,6 @@ class AntiSpamMiddleware(BaseMiddleware):
user=user_str
)
# Мягкое превышение лимита
if relevant_requests >= effective_limit:
user_stats.add_warning()
spam_stats.total_warnings_issued += 1
@@ -505,7 +500,6 @@ class AntiSpamMiddleware(BaseMiddleware):
user=user_str
)
# Блокировка при достижении лимита варнингов
if user_stats.warnings >= self.warning_limit:
block_duration = self._calculate_block_duration(user_stats.warnings)
user_stats.block(current_time, block_duration)
@@ -526,7 +520,7 @@ class AntiSpamMiddleware(BaseMiddleware):
if isinstance(event, Message):
try:
await event.answer(block_message, parse_mode="HTML")
except:
except Exception:
pass
elif isinstance(event, CallbackQuery):
await event.answer(
@@ -536,7 +530,6 @@ class AntiSpamMiddleware(BaseMiddleware):
return None
# Предупреждение (только для сообщений, не для callback)
if isinstance(event, Message):
warning_message = (
f"⚠️ <b>Предупреждение {user_stats.warnings}/{self.warning_limit}</b>\n\n"
@@ -544,7 +537,7 @@ class AntiSpamMiddleware(BaseMiddleware):
)
try:
await event.answer(warning_message, parse_mode="HTML")
except:
except Exception:
pass
return None