This commit is contained in:
2026-02-20 08:34:50 +07:00
parent 5aca4e8438
commit 4d1b8911b3
4 changed files with 274 additions and 162 deletions

View File

@@ -1,11 +1,16 @@
""" """
Middleware для проверки сообщений на запрещённые слова (банворды). Middleware для проверки сообщений на запрещённые слова (банворды).
...
✅ ИСПРАВЛЕНО: ✅ ИСПРАВЛЕНО:
- ❌ PatternError: bad character range 🀀-\\\\ (исправлено экранирование Unicode) - Полная нормализация текста с использованием UNICODE_MAP
- НЕТ уведомлений в режиме тишины - Удаление повторов символов (леееейн → лейн)
- Игнорирование разделителей (л.е.й.н → лейн)
- Поддержка всех типов проверок (SUBSTRING, LEMMA, PART, CONFLICT)
- Белый список и режимы тишины/конфликта
- Нет уведомлений в режиме тишины
""" """
from typing import Callable, Dict, Any, Awaitable, Optional
from typing import Callable, Dict, Any, Awaitable, Optional, Set
import re import re
import unicodedata import unicodedata
@@ -13,7 +18,7 @@ from aiogram import BaseMiddleware
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from configs import settings from configs import settings, UNICODE_MAP, LATIN_TO_CYRILLIC, CYRILLIC_NORMALIZE
from database import get_manager, BanWordType from database import get_manager, BanWordType
from bot.special import process_text, extract_words, get_lemma from bot.special import process_text, extract_words, get_lemma
from middleware.loggers import logger from middleware.loggers import logger
@@ -21,10 +26,102 @@ from middleware.loggers import logger
__all__ = ("BanWordsMiddleware",) __all__ = ("BanWordsMiddleware",)
class TextNormalizer:
"""
Класс для многоступенчатой нормализации текста.
Приводит различные юникод-символы к базовым буквам,
удаляет повторы, убирает разделители.
"""
# Объединяем все словари замен в один
FULL_MAP = {}
FULL_MAP.update(LATIN_TO_CYRILLIC)
FULL_MAP.update(CYRILLIC_NORMALIZE)
FULL_MAP.update(UNICODE_MAP)
# Символы-разделители, которые могут быть вставлены между буквами
SEPARATORS = re.compile(r'[\s\.\-_,;:|]+', re.UNICODE)
# Паттерн для поиска повторяющихся букв (3+ раза)
REPEAT_PATTERN = re.compile(r'([а-яёa-z])\1{2,}', re.IGNORECASE)
@classmethod
def normalize_characters(cls, text: str) -> str:
"""
Заменяет все символы из FULL_MAP на их базовые эквиваленты.
Проходит по строке посимвольно для максимальной замены.
"""
result = []
for ch in text:
# Сначала пробуем заменить по карте
if ch in cls.FULL_MAP:
result.append(cls.FULL_MAP[ch])
else:
result.append(ch)
# Приводим к нижнему регистру после замен (чтобы избежать потери регистра в карте)
return ''.join(result).lower()
@classmethod
def remove_separators(cls, text: str) -> str:
"""Удаляет разделители между буквами (пробелы, точки и т.д.)"""
return cls.SEPARATORS.sub('', text)
@classmethod
def collapse_repeats(cls, text: str, max_repeat: int = 2) -> str:
"""
Заменяет повторения символов более max_repeat подряд на один/два символа.
По умолчанию оставляет максимум 2 (леееейн → леейн? но обычно хватит 2).
Можно настроить: для банворда "лейн" превратит "леееейн" в "леейн", что всё равно содержит "лейн".
"""
def repl(m):
ch = m.group(1)
# Оставляем два символа, чтобы не терять удвоенные буквы (например, "дд" в слове "поддон")
return ch * 2
return cls.REPEAT_PATTERN.sub(repl, text)
@classmethod
def normalize_full(cls, text: str, remove_sep: bool = True, collapse: bool = True) -> str:
"""
Полная нормализация:
1. Unicode нормализация (NFKC) для разложения составных символов
2. Замена по карте
3. Приведение к нижнему регистру
4. Удаление разделителей (опционально)
5. Схлопывание повторов (опционально)
"""
# NFKC разлагает символы типа "ё" в "е" + умляут, но нам лучше оставить как есть,
# т.к. у нас есть прямые замены. Однако для совместимости применим.
text = unicodedata.normalize('NFKC', text)
# Замена символов
text = cls.normalize_characters(text)
# Удаление разделителей
if remove_sep:
text = cls.remove_separators(text)
# Схлопывание повторов
if collapse:
text = cls.collapse_repeats(text)
return text
@classmethod
def normalize_for_part(cls, text: str) -> str:
"""
Нормализация для типа PART:
- Полная нормализация
- Удаление всех не-буквенных символов (кроме пробелов)
- Приведение к нижнему регистру
"""
text = cls.normalize_full(text, remove_sep=False, collapse=True)
# Оставляем только буквы и пробелы
text = re.sub(r'[^а-яёa-z\s]', '', text, flags=re.IGNORECASE)
text = re.sub(r'\s+', ' ', text).strip()
return text.lower()
class BanWordsMiddleware(BaseMiddleware): class BanWordsMiddleware(BaseMiddleware):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.manager = get_manager() self.manager = get_manager()
self.normalizer = TextNormalizer()
async def __call__( async def __call__(
self, self,
@@ -32,53 +129,110 @@ class BanWordsMiddleware(BaseMiddleware):
event: Message, event: Message,
data: Dict[str, Any] data: Dict[str, Any]
) -> Any: ) -> Any:
# Проверяем наличие текста или подписи
if not event.text and not event.caption: if not event.text and not event.caption:
return await handler(event, data) return await handler(event, data)
message_text = event.text or event.caption message_text = event.text or event.caption
# Игнорируем команды
if message_text.startswith('/'): if message_text.startswith('/'):
return await handler(event, data) return await handler(event, data)
# Админ проверка # Проверка на админа
user_id = event.from_user.id user_id = event.from_user.id
is_super_admin = user_id in settings.OWNER_ID is_super_admin = user_id in settings.OWNER_ID
is_admin = is_super_admin or self.manager.is_admin_cached(user_id) is_admin = is_super_admin or self.manager.is_admin_cached(user_id)
if is_admin: if is_admin:
return await handler(event, data) return await handler(event, data)
# Проверяем сообщение на спам
spam_result = await self._check_message(message_text) spam_result = await self._check_message(message_text)
if spam_result: if spam_result:
await self._handle_spam(event, spam_result) await self._handle_spam(event, spam_result)
return None return None # Сообщение удалено, дальше не обрабатываем
return await handler(event, data) return await handler(event, data)
@staticmethod async def _check_message(self, text: str) -> Optional[Dict[str, str]]:
def _normalize_universal(text: str, mode: str = "strict") -> str: """
"""✅ ИСПРАВЛЕНО: Универсальная нормализация для всех типов проверок""" Многоступенчатая проверка текста.
# БЕЗОПАСНАЯ нормализация - убираем все проблемные символы Возвращает словарь с причиной блокировки или None.
text = unicodedata.normalize('NFKC', text) """
# 1. Повторяющиеся символы (например, "леееейн") — блокируем сразу
if mode == "strict": # PART - сохраняем буквы, цифры, пробелы repeat_result = self._check_repeated_chars(text)
# ✅ ИСПРАВЛЕНО: безопасный паттерн только для букв/цифр/пробелов if repeat_result:
text = re.sub(r'[^a-zA-Zа-яА-ЯёЁ0-9\s]', '', text) return repeat_result
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 # 2. Получаем кэшированные списки
def _normalize_repeated_chars(text: str) -> str: substring_words = self.manager.get_banwords_cached(BanWordType.SUBSTRING)
"""Убирает повторения >2 (лееееин → лейн)""" lemma_words = self.manager.get_banwords_cached(BanWordType.LEMMA)
return re.sub(r'([а-яёa-z])\\1{2,}', r'\\1\\1', text, flags=re.IGNORECASE) part_words = self.manager.get_banwords_cached(BanWordType.PART)
conflict_substring = self.manager.get_banwords_cached(BanWordType.CONFLICT_SUBSTRING)
conflict_lemma = self.manager.get_banwords_cached(BanWordType.CONFLICT_LEMMA)
# 3. Белый список
if self.manager.is_whitelisted(text):
logger.debug(f"⏭️ Пропуск по белому списку: {text[:30]}", log_type="BANWORDS")
return None
# 4. Режим тишины
if await self.manager.is_silence_active():
return {"word": "[режим тишины]", "type": "silence"}
# 5. Режим конфликта (более мягкие правила)
if await self.manager.is_conflict_active():
# Проверка conflict_substring (с нормализацией)
normalized_text = self.normalizer.normalize_full(text, remove_sep=True, collapse=True)
for word in conflict_substring:
norm_word = self.normalizer.normalize_full(word, remove_sep=True, collapse=True)
if norm_word in normalized_text:
return {"word": word, "type": "conflict_substring"}
# conflict_lemma
for word_text in extract_words(text):
lemma = get_lemma(word_text)
if lemma in conflict_lemma:
return {"word": lemma, "type": "conflict_lemma"}
# Если в конфликтном режиме ничего не найдено — пропускаем
return None
# 6. Обычный режим: проверка substring (с удалением разделителей и схлопыванием повторов)
normalized_text = self.normalizer.normalize_full(text, remove_sep=True, collapse=True)
for word in substring_words:
norm_word = self.normalizer.normalize_full(word, remove_sep=True, collapse=True)
if norm_word in normalized_text:
logger.info(f"✅ SUBSTRING: '{word}'", log_type="BANWORDS")
return {"word": word, "type": "substring"}
# 7. Проверка part (строгая нормализация, только буквы и пробелы)
part_normalized = self.normalizer.normalize_for_part(text)
for part in part_words:
norm_part = self.normalizer.normalize_for_part(part)
if norm_part in part_normalized:
logger.info(f"✅ PART: '{part}'", log_type="BANWORDS")
return {"word": part, "type": "part"}
# 8. Проверка lemma
for word_text in extract_words(text):
# Для леммы тоже применяем нормализацию (удаляем разделители, схлопываем повторы)
normalized_word = self.normalizer.normalize_full(word_text, remove_sep=True, collapse=True)
lemma = get_lemma(normalized_word)
if lemma in lemma_words:
logger.info(f"✅ LEMMA: '{lemma}' из '{word_text}'", log_type="BANWORDS")
return {"word": lemma, "type": "lemma"}
return None
def _check_repeated_chars(self, text: str) -> Optional[Dict[str, str]]: def _check_repeated_chars(self, text: str) -> Optional[Dict[str, str]]:
"""🔥 Блокирует 3+ повторяющиеся символы подряд""" """
# ✅ ИСПРАВЛЕНО: безопасный паттерн только для букв Проверяет на наличие 3+ повторяющихся букв подряд.
pattern = r'([а-яёa-zA-Z])\\1{2,}' Использует сырой текст без нормализации (чтобы поймать "леееейн").
matches = re.finditer(pattern, text, flags=re.IGNORECASE) """
# Ищем повторения букв (только кириллица/латиница)
pattern = re.compile(r'([а-яёa-zA-Z])\1{2,}', re.IGNORECASE)
matches = pattern.finditer(text)
for match in matches: for match in matches:
char = match.group(1) char = match.group(1)
count = len(match.group(0)) count = len(match.group(0))
@@ -87,97 +241,31 @@ class BanWordsMiddleware(BaseMiddleware):
return {"word": f"'{match.group(0)}' ({count}x)", "type": "repeated_chars"} return {"word": f"'{match.group(0)}' ({count}x)", "type": "repeated_chars"}
return None return None
async def _check_message(self, text: str) -> Optional[Dict[str, str]]:
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)
logger.debug(
f"🔍 | universal='{text_universal}' | loose='{text_loose}' | proc='{text_processed}'",
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
# 4. SILENCE MODE
if await self.manager.is_silence_active():
return {"word": "[режим тишины]", "type": "silence"}
# 5. CONFLICT MODE
if await self.manager.is_conflict_active():
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)
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"}
# 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"}
# 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"}
# 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"}
return None
async def _handle_spam(self, message: Message, spam_result: Dict[str, str]) -> None: async def _handle_spam(self, message: Message, spam_result: Dict[str, str]) -> None:
"""Обрабатывает спам-сообщение: удаляет, логирует, уведомляет (кроме silence)"""
user = message.from_user user = message.from_user
matched_word = spam_result["word"] matched_word = spam_result["word"]
match_type = spam_result["type"] match_type = spam_result["type"]
message_text = message.text or message.caption or "[нет текста]" message_text = message.text or message.caption or "[нет текста]"
# ✅ ПРОВЕРКА: НЕ отправляем уведомления в режиме тишины # В режиме тишины удаляем молча
if match_type == "silence": if match_type == "silence":
# Удаляем сообщение молча
try: try:
await message.delete() await message.delete()
logger.info(f"🔇 SILENCE: @{user.username or user.id} удалено молча", logger.info(f"🔇 SILENCE: @{user.username or user.id} удалено молча", log_type="BANWORDS")
log_type="BANWORDS")
except TelegramBadRequest as e: except TelegramBadRequest as e:
logger.error(f"Не удалено (silence): {e}", log_type="BANWORDS") logger.error(f"Не удалено (silence): {e}", log_type="BANWORDS")
return return
# Удаляем # Удаляем сообщение
try: try:
await message.delete() await message.delete()
logger.info(f"🚫 @{user.username or user.id}: '{matched_word}' ({match_type})", logger.info(f"🚫 @{user.username or user.id}: '{matched_word}' ({match_type})", log_type="BANWORDS")
log_type="BANWORDS")
except TelegramBadRequest as e: except TelegramBadRequest as e:
logger.error(f"Не удалено: {e}", log_type="BANWORDS") logger.error(f"Не удалено: {e}", log_type="BANWORDS")
return return
# Логируем в БД (только НЕ silence) # Логируем в БД
await self.manager.log_spam( await self.manager.log_spam(
user_id=user.id, user_id=user.id,
username=user.username or f"id{user.id}", username=user.username or f"id{user.id}",
@@ -187,17 +275,17 @@ class BanWordsMiddleware(BaseMiddleware):
match_type=match_type match_type=match_type
) )
# Уведомляем админов (только НЕ silence) # Уведомляем админов
await self._notify_admins(message, matched_word, match_type, message_text) await self._notify_admins(message, matched_word, match_type, message_text)
# Остальные методы без изменений...
async def _notify_admins( async def _notify_admins(
self, self,
message: Message, message: Message,
matched_word: str, matched_word: str,
match_type: str, match_type: str,
message_text: str message_text: str
) -> None: ) -> None:
"""Отправляет уведомление об удалении в админ-чат (берёт ID из БД)"""
user = message.from_user user = message.from_user
username = f"@{user.username}" if user.username else f"ID: {user.id}" username = f"@{user.username}" if user.username else f"ID: {user.id}"
spam_count = await self.manager.get_user_spam_count(user.id) spam_count = await self.manager.get_user_spam_count(user.id)
@@ -205,17 +293,17 @@ class BanWordsMiddleware(BaseMiddleware):
source_thread_id = message.message_thread_id source_thread_id = message.message_thread_id
notification_text = ( notification_text = (
f"🚫 <b>Удалено сообщение</b>\\n\\n" f"🚫 <b>Удалено сообщение</b>\n\n"
f"👤 <b>Пользователь:</b> {username}\\n" f"👤 <b>Пользователь:</b> {username}\n"
f"🆔 <b>ID:</b> <code>{user.id}</code>\\n" f"🆔 <b>ID:</b> <code>{user.id}</code>\n"
f"📊 <b>Нарушений:</b> {spam_count}\\n\\n" f"📊 <b>Нарушений:</b> {spam_count}\n\n"
f"💬 <b>Чат:</b> {self._escape_html(chat_title)}\\n" f"💬 <b>Чат:</b> {self._escape_html(chat_title)}\n"
f"🆔 <b>Chat ID:</b> <code>{message.chat.id}</code>\\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>Topic ID:</b> <code>' + str(source_thread_id) + '</code>\n' if source_thread_id else ''}"
f"🔗 <b>Message ID:</b> <code>{message.message_id}</code>\\n\\n" 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> <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> {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>" f"💬 <b>Текст:</b>\n<code>{self._escape_html(message_text[:500])}</code>"
) )
keyboard = InlineKeyboardMarkup(inline_keyboard=[ keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -227,16 +315,18 @@ class BanWordsMiddleware(BaseMiddleware):
]) ])
try: try:
admin_chat_id = getattr(settings, "ADMIN_CHAT_ID", None) # ✅ Получаем настройки из БД (динамические, установленные через /settings)
admin_thread_id = getattr(settings, "ADMIN_THREAD_ID", None) or None admin_chat_id = await self.manager.get_bot_setting("admin_chat_id")
admin_thread_id = await self.manager.get_bot_setting("admin_thread_id")
await message.bot.send_message( if admin_chat_id:
chat_id=admin_chat_id, await message.bot.send_message(
text=notification_text, chat_id=int(admin_chat_id),
reply_markup=keyboard, text=notification_text,
parse_mode="HTML", reply_markup=keyboard,
message_thread_id=admin_thread_id parse_mode="HTML",
) message_thread_id=int(admin_thread_id) if admin_thread_id else None
)
except Exception as e: except Exception as e:
logger.error(f"❌ Уведомление админам: {e}", log_type="BANWORDS") logger.error(f"❌ Уведомление админам: {e}", log_type="BANWORDS")

View File

@@ -26,6 +26,7 @@ class _Settings(BaseSettings):
# Параметры сообщений # Параметры сообщений
PARSE_MODE: str = "HTML" PARSE_MODE: str = "HTML"
PREFIX: str = "/!.&?" PREFIX: str = "/!.&?"
LOG_LEVEL: str = "TRACE"
# Разрешения и логирование # Разрешения и логирование
BOT_EDIT: bool = False BOT_EDIT: bool = False

View File

@@ -813,7 +813,7 @@ class BanWordsManager:
return settings.get(key) return settings.get(key)
async def init_default_bot_settings(self) -> None: async def init_default_bot_settings(self) -> None:
"""Инициализирует настройки по умолчанию из .env""" """Инициализирует настройки по умолчанию из .env, только если они ещё не установлены"""
try: try:
from configs import settings from configs import settings
@@ -825,10 +825,15 @@ class BanWordsManager:
} }
for key, value in defaults.items(): for key, value in defaults.items():
if value: # Не null if value is not None: # В .env значение задано
await self.set_bot_setting(key, str(value)) existing = await self.get_bot_setting(key)
if existing is None:
await self.set_bot_setting(key, str(value))
logger.debug(f"Установлена настройка {key} из .env", log_type="SETTINGS")
else:
logger.debug(f"Настройка {key} уже существует ({existing}), пропускаем", log_type="SETTINGS")
logger.info("✅ Настройки бота инициализированы из .env", log_type="SETTINGS") logger.info("✅ Настройки бота инициализированы из .env (существующие сохранены)", log_type="SETTINGS")
except Exception as e: except Exception as e:
logger.warning(f"Не удалось инициализировать настройки из .env: {e}", log_type="SETTINGS") logger.warning(f"Не удалось инициализировать настройки из .env: {e}", log_type="SETTINGS")

View File

@@ -1,5 +1,5 @@
""" """
Кастомный логгер с поддержкой декораторов и прямого вызова Кастомный логгер с поддержством декораторов и прямого вызова
""" """
from sys import stderr from sys import stderr
from pathlib import Path from pathlib import Path
@@ -43,7 +43,6 @@ class Logger:
'<cyan>{extra[user]}</cyan> <red>|</red> <level>{message}</level>' '<cyan>{extra[user]}</cyan> <red>|</red> <level>{message}</level>'
) )
def __init__(self, system_name: str = 'PRIMO') -> None: def __init__(self, system_name: str = 'PRIMO') -> None:
""" """
Инициализация логгера. Инициализация логгера.
@@ -58,6 +57,11 @@ class Logger:
""" """
Настройка обработчиков Loguru: консоль и файлы. Настройка обработчиков Loguru: консоль и файлы.
Учитывает переменную LOG_LEVEL из settings.
LOG_LEVEL определяет минимальный уровень для консоли и общего файла,
а также влияет на то, какие отдельные файлы создаются:
создаются только файлы для уровней >= LOG_LEVEL.
Args: Args:
start: Если True, сразу логирует запуск проекта start: Если True, сразу логирует запуск проекта
""" """
@@ -67,6 +71,15 @@ class Logger:
# Полная очистка настроек # Полная очистка настроек
nlogger.remove() nlogger.remove()
# Определяем уровень логирования из настроек
log_level_str = getattr(settings, 'LOG_LEVEL', 'INFO').upper()
# Проверка на допустимость
try:
log_level_no = nlogger.level(log_level_str).no
except ValueError:
log_level_str = 'INFO'
log_level_no = nlogger.level('INFO').no
# Создание директории для файловых логов # Создание директории для файловых логов
log_dir: Path = settings.LOG_DIR log_dir: Path = settings.LOG_DIR
if not log_dir.exists(): if not log_dir.exists():
@@ -78,45 +91,49 @@ class Logger:
sink=stderr, sink=stderr,
format=self._log_format, format=self._log_format,
colorize=True, colorize=True,
level='INFO', level=log_level_str,
filter=lambda rec: rec['extra'].get('log_type') != 'TRACE' filter=lambda rec: rec['extra'].get('log_type') != 'TRACE'
) )
# Файловые логи # Файловые логи
if settings.LOG_FILE: if settings.LOG_FILE:
# Общий лог со всеми уровнями # Общий лог со всеми уровнями (начиная с LOG_LEVEL)
nlogger.add( nlogger.add(
sink=log_dir / 'bot.log', sink=log_dir / 'bot.log',
rotation=settings.LOG_ROTATION, rotation=settings.LOG_ROTATION,
retention=settings.LOG_RETENTION, retention=settings.LOG_RETENTION,
format=self._log_format, format=self._log_format,
level='DEBUG', level=log_level_str,
enqueue=True, enqueue=True,
backtrace=True, backtrace=True,
diagnose=True, diagnose=True,
encoding='utf-8' encoding='utf-8'
) )
# Раздельные логи по уровням # Раздельные логи по уровням создаём только для уровней >= LOG_LEVEL
log_levels = { # Список интересующих нас уровней (в порядке возрастания)
'INFO': 'info.log', level_configs = [
'WARNING': 'warning.log', ('DEBUG', 'debug.log'),
'ERROR': 'error.log', ('INFO', 'info.log'),
'CRITICAL': 'critical.log' ('SUCCESS', 'success.log'),
} ('WARNING', 'warning.log'),
('ERROR', 'error.log'),
('CRITICAL', 'critical.log')
]
for level_name, filename in level_configs:
for level_name, filename in log_levels.items(): level_no = nlogger.level(level_name).no
nlogger.add( if level_no >= log_level_no:
sink=log_dir / filename, nlogger.add(
rotation='10 MB', sink=log_dir / filename,
retention=settings.LOG_RETENTION, rotation='10 MB',
format=self._log_format, retention=settings.LOG_RETENTION,
level=level_name, format=self._log_format,
filter=lambda rec, lvl=level_name: rec['level'].name == lvl, level=level_name,
enqueue=True, filter=lambda rec, lvl=level_name: rec['level'].name == lvl,
encoding='utf-8' enqueue=True,
) encoding='utf-8'
)
self._setup_done = True self._setup_done = True
@@ -128,7 +145,6 @@ class Logger:
log_type='START' log_type='START'
) )
@staticmethod @staticmethod
def format_user(event: Optional[EventType] = None) -> str: def format_user(event: Optional[EventType] = None) -> str:
""" """
@@ -322,8 +338,8 @@ class Logger:
user: Optional[str] = None, user: Optional[str] = None,
message: Optional[EventType] = None message: Optional[EventType] = None
) -> None: ) -> None:
"""Логирование успешного выполнения (уровень INFO)""" """Логирование успешного выполнения (уровень SUCCESS)"""
self.log_entry('INFO', f"{text}", log_type, user, message) self.log_entry('SUCCESS', text, log_type, user, message)
# ================= КОНТЕКСТНЫЕ МЕНЕДЖЕРЫ ================= # ================= КОНТЕКСТНЫЕ МЕНЕДЖЕРЫ =================