diff --git a/bot/filters/callback.py b/bot/filters/callback.py index ad52fdc..7064ea8 100644 --- a/bot/filters/callback.py +++ b/bot/filters/callback.py @@ -23,7 +23,7 @@ class CallbackStartsWith(BaseFilter): Проверяет, начинается ли callback_data с указанного префикса. Attributes: - prefix: Префикс для проверки (строка или список строк) + № prefix: Префикс для проверки (строка или список строк) ignore_case: Игнорировать регистр Example: @@ -172,7 +172,7 @@ class CallbackMatches(BaseFilter): Example: ```python # Паттерн: user_123, user_456 и т.д. - @router.callback_query(CallbackMatches(r"^user_(\d+)$")) + @router.callback_query(CallbackMatches(r'^user_(\\d+)$')) async def user_handler(callback: CallbackQuery, matched: dict): user_id = matched['groups'] await callback.answer(f"Пользователь {user_id}") diff --git a/bot/handlers/commands/users/conflict.py b/bot/handlers/commands/users/conflict.py index 8e75b95..5d2ea8a 100644 --- a/bot/handlers/commands/users/conflict.py +++ b/bot/handlers/commands/users/conflict.py @@ -99,7 +99,7 @@ async def add_conflict_word_cmd(message: Message) -> None: try: added = await manager.add_banword( word=word, - word_type=BanWordType.CONFLICT_SUBSTRING, + word_type=BanWordType.CONFLICT_WORD, added_by=message.from_user.id, reason="Конфликтное слово" ) @@ -192,7 +192,7 @@ async def remove_conflict_word_cmd(message: Message) -> None: try: removed = await manager.remove_banword( word=word, - word_type=BanWordType.CONFLICT_SUBSTRING + word_type=BanWordType.CONFLICT_WORD ) if removed: @@ -433,3 +433,113 @@ async def conflict_status_cmd(message: Message) -> None: except Exception as e: logger.error(f"Ошибка получения статуса режима: {e}", log_type="CONFLICT") await message.answer("❌ Ошибка получения статуса", parse_mode="HTML") + + +@router.message( + Command(*COMMANDS.get("addconflictpart", ["addconflictpart"]), + prefix=settings.PREFIX, + ignore_case=True), + IsAdmin() +) +@log_action(action_name="ADD_CONFLICT_PART", log_args=True) +async def add_conflict_part_cmd(message: Message) -> None: + """ + Добавляет конфликтную часть. + + Использование: /addconflictpart <комбинация> + """ + success, result = parse_conflict_args( + message.text, + "addconflictpart", + need_minutes=False + ) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + added = await manager.add_banword( + word=word, + word_type=BanWordType.CONFLICT_PART, + added_by=message.from_user.id, + reason="Конфликтная часть" + ) + + if added: + text = ( + f"✅ Конфликтная часть добавлена\n\n" + f"🧩 Часть: {word}\n" + f"🔍 Тип: поиск без пробелов\n\n" + f"⚔️ Работает только в режиме антиконфликта\n" + f"Активируйте: /stopconflict [минуты]" + ) + else: + text = f"⚠️ Конфликтная часть {word} уже существует" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error( + f"Ошибка добавления конфликтной части: {e}", + log_type="CONFLICT" + ) + await message.answer( + "❌ Ошибка добавления\n\nПопробуйте позже", + parse_mode="HTML" + ) + +@router.message( + Command(*COMMANDS.get("remconflictpart", ["remconflictpart"]), + prefix=settings.PREFIX, + ignore_case=True), + IsAdmin() +) +@log_action(action_name="REMOVE_CONFLICT_PART", log_args=True) +async def remove_conflict_part_cmd(message: Message) -> None: + """ + Удаляет конфликтную часть. + + Использование: /remconflictpart <комбинация> + """ + success, result = parse_conflict_args( + message.text, + "remconflictpart", + need_minutes=False + ) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + removed = await manager.remove_banword( + word=word, + word_type=BanWordType.CONFLICT_PART + ) + + if removed: + text = ( + f"🗑 Конфликтная часть удалена\n\n" + f"🧩 Часть: {word}" + ) + else: + text = f"⚠️ Конфликтная часть {word} не найдена" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error( + f"Ошибка удаления конфликтной части: {e}", + log_type="CONFLICT" + ) + await message.answer( + "❌ Ошибка удаления\n\nПопробуйте позже", + parse_mode="HTML" + ) diff --git a/bot/handlers/commands/users/listwords.py b/bot/handlers/commands/users/listwords.py index 4f99db3..28117cd 100644 --- a/bot/handlers/commands/users/listwords.py +++ b/bot/handlers/commands/users/listwords.py @@ -7,7 +7,6 @@ from aiogram.types import Message, CallbackQuery from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.exceptions import TelegramBadRequest -from bot.filters.admin import IsAdmin from configs import settings, COMMANDS from database import get_manager from middleware.loggers import logger @@ -27,7 +26,7 @@ def get_refresh_kb(page: int = 0): return ikb.as_markup() -async def format_banwords_list(page: int = 0) -> str: +async def format_banwords_list() -> str: """ Форматирует список всех банвордов с разбивкой по типам. @@ -46,13 +45,14 @@ async def format_banwords_list(page: int = 0) -> str: stats = await manager.get_stats() # Извлекаем данные из словаря - permanent_words = list(data.get('substring', set())) + permanent_words = list(data.get('word', set())) permanent_lemmas = list(data.get('lemma', set())) permanent_parts = list(data.get('part', set())) - temp_words = list(data.get('temp_substring', set())) + temp_words = list(data.get('temp_word', set())) temp_lemmas = list(data.get('temp_lemma', set())) - conflict_words = list(data.get('conflict_substring', set())) + conflict_words = list(data.get('conflict_word', set())) conflict_lemmas = list(data.get('conflict_lemma', set())) + conflict_parts = list(data.get('conflict_part', set())) exceptions = list(data.get('whitelist', set())) except Exception as e: @@ -67,7 +67,7 @@ async def format_banwords_list(page: int = 0) -> str: total_count = ( len(permanent_words) + len(permanent_lemmas) + len(permanent_parts) + len(temp_words) + len(temp_lemmas) + - len(conflict_words) + len(conflict_lemmas) + len(conflict_words) + len(conflict_lemmas) + len(conflict_parts) ) output += f"📊 Общая статистика:\n" @@ -81,21 +81,21 @@ async def format_banwords_list(page: int = 0) -> str: output += "🔴 ПОСТОЯННЫЕ ПРАВИЛА:\n\n" if permanent_words: - output += f"📝 Подстроки ({len(permanent_words)}):\n" + output += f"📝 Слова ({len(permanent_words)}):\n" words_str = ', '.join([f"{w}" for w in sorted(permanent_words)[:20]]) if len(permanent_words) > 20: words_str += f" ... (+{len(permanent_words) - 20} ещё)" output += f"{words_str}\n\n" if permanent_lemmas: - output += f"🔤 Леммы ({len(permanent_lemmas)}):\n" + output += f"🔤 Леммы (морф.формы) ({len(permanent_lemmas)}):\n" lemmas_str = ', '.join([f"{w}" for w in sorted(permanent_lemmas)[:20]]) if len(permanent_lemmas) > 20: lemmas_str += f" ... (+{len(permanent_lemmas) - 20} ещё)" output += f"{lemmas_str}\n\n" if permanent_parts: - output += f"🧩 Части ({len(permanent_parts)}):\n" + output += f"🧩 Части в сообщении ({len(permanent_parts)}):\n" parts_str = ', '.join([f"{w}" for w in sorted(permanent_parts)[:20]]) if len(permanent_parts) > 20: parts_str += f" ... (+{len(permanent_parts) - 20} ещё)" @@ -106,7 +106,7 @@ async def format_banwords_list(page: int = 0) -> str: output += "⏱ ВРЕМЕННЫЕ ПРАВИЛА:\n\n" if temp_words: - output += f"📝 Временные подстроки ({len(temp_words)}):\n" + output += f"📝 Временные слова ({len(temp_words)}):\n" # Для временных слов нужна дополнительная информация о времени истечения # Пока просто выводим список words_str = ', '.join([f"{w}" for w in sorted(temp_words)[:15]]) @@ -122,7 +122,7 @@ async def format_banwords_list(page: int = 0) -> str: output += f"{lemmas_str}\n\n" # === КОНФЛИКТНЫЕ ПРАВИЛА === - if conflict_words or conflict_lemmas: + if conflict_words or conflict_lemmas or conflict_parts: output += "⚔️ КОНФЛИКТНЫЕ ПРАВИЛА:\n" output += "(работают только в режиме /stopconflict время)\n\n" @@ -140,10 +140,17 @@ async def format_banwords_list(page: int = 0) -> str: lemmas_str += f" ... (+{len(conflict_lemmas) - 15} ещё)" output += f"{lemmas_str}\n\n" + if conflict_parts: + output += f"🧩 Конфликтные части ({len(conflict_parts)}):\n" + parts_str = ', '.join([f"{w}" for w in sorted(conflict_parts)[:15]]) + if len(conflict_parts) > 15: + parts_str += f" ... (+{len(conflict_parts) - 15} ещё)" + output += f"{parts_str}\n\n" + # === ИСКЛЮЧЕНИЯ (WHITELIST) === if exceptions: output += f"✅ ИСКЛЮЧЕНИЯ ({len(exceptions)}):\n" - exc_str = ', '.join([f"{exceptions}" for w in sorted(exceptions)[:15]]) + exc_str = ', '.join([f"{w}" for w in sorted(exceptions)[:15]]) if len(exceptions) > 15: exc_str += f" ... (+{len(exceptions) - 15} ещё)" output += f"{exc_str}\n\n" @@ -183,7 +190,7 @@ async def format_banwords_list(page: int = 0) -> str: @router.callback_query(F.data.startswith("listwords:refresh")) -@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin()) +@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True)) @log_action(action_name="LISTWORDS_COMMAND") async def listwords_cmd(update: Message | CallbackQuery) -> None: """ @@ -209,7 +216,7 @@ async def listwords_cmd(update: Message | CallbackQuery) -> None: # Формируем список try: - text = await format_banwords_list(page) + text = await format_banwords_list() keyboard = get_refresh_kb(page) if is_callback: diff --git a/bot/handlers/commands/users/start_cmd.py b/bot/handlers/commands/users/start_cmd.py index a8aa4c1..8f814cc 100644 --- a/bot/handlers/commands/users/start_cmd.py +++ b/bot/handlers/commands/users/start_cmd.py @@ -53,13 +53,13 @@ async def start_cmd(update: Message | CallbackQuery) -> None: # Формируем текст помощи help_text = ( - f'{tg_emoji(4961073056677103064)} PrimoGuard - Бот-модератор\n\n' - '
Автоматическое удаление сообщений с запрещёнными словами.\nПоддержка подстрок, лемм, временных блокировок и режимов модерации.
\n\n' + f'{tg_emoji("4961073056677103064")} PrimoGuard - Бот-модератор\n\n' + '
Автоматическое удаление сообщений с запрещёнными словами.\nПоддержка слов, лемм, временных блокировок и режимов модерации.
\n\n' ) # === Команды просмотра === help_text += ( - f'{tg_emoji(4961141003059725568)} Просмотр:\n' + f'{tg_emoji("4961141003059725568")} Просмотр:\n' '/list — список всех правил и слов\n' '/stats — статистика по удалениям\n' '/id — получение айди пользователя\n' @@ -68,23 +68,23 @@ async def start_cmd(update: Message | CallbackQuery) -> None: # === Постоянные банворды === help_text += ( - f'{tg_emoji(4961019408240608234)} Добавить банворд (постоянно):\n' - '/addword слово — подстрока (простой поиск)\n' - '/addlemma слово — лемма (все формы слова)\n' - '/addpart комбинация — часть (поиск без пробелов)\n\n' + f'{tg_emoji("4961019408240608234")} Добавить банворд (постоянно):\n' + '/word слово — слова (простой поиск)\n' + '/lemma слово — лемма (все формы слова)\n' + '/part комбинация — часть (поиск без пробелов)\n\n' ) # === Временные банворды === help_text += ( - f'{tg_emoji(4960719190026618714)} Добавить банворд (временно):\n' - '/addtempword слово минуты — временная подстрока\n' - '/addtemplemma слово минуты — временная лемма\n' - 'Пример: /addtempword спам 60\n\n' + f'{tg_emoji("4960719190026618714")} Добавить банворд (временно):\n' + '/tempword слово минуты — временная слова\n' + '/templemma слово минуты — временная лемма\n' + 'Пример: /tempword спам 60\n\n' ) # === Исключения (whitelist) === help_text += ( - f'{tg_emoji(4963010134172239128)} Исключения (whitelist):\n' + f'{tg_emoji("4963010134172239128")} Исключения (whitelist):\n' '/addexcept текст — добавить исключение\n' '/remexcept текст — удалить исключение\n' 'Исключения не проверяются фильтром\n\n' @@ -92,36 +92,38 @@ async def start_cmd(update: Message | CallbackQuery) -> None: # === Режимы модерации === help_text += ( - f'{tg_emoji(4960987543878239236)} Режим тишины:\n' + f'{tg_emoji("4960987543878239236")} Режим тишины:\n' '/silence минуты — удалять ВСЕ сообщения\n' '/unsilence — отключить режим тишины\n' '/report — отправить репорт\n\n' ) help_text += ( - f'{tg_emoji(4960986152308835400)} Режим антиконфликта:\n' + f'{tg_emoji("4960986152308835400")} Режим антиконфликта:\n' '/addconflictword слово — добавить конфликтное слово\n' '/addconflictlemma слово — добавить конфликтную лемму\n' + '/addconflictpart слово — добавить конфликтную часть\n' '/stopconflict минуты — активировать режим\n' '/unstopconflict — отключить режим\n\n' ) # === Удаление === help_text += ( - f'{tg_emoji(4961196485447254983)} Удалить:\n' - '/remword слово — удалить подстроку\n' + f'{tg_emoji("4961196485447254983")} Удалить:\n' + '/remword слово — удалить слову\n' '/remlemma слово — удалить лемму\n' '/rempart комбинация — удалить часть\n' - '/remtempword слово — удалить временную подстроку\n' + '/remtempword слово — удалить временную слову\n' '/remtemplemma слово — удалить временную лемму\n' '/remconflictword слово — удалить конфликтное слово\n' + '/remconflictpart слово — удалить конфликтное часть\n' '/remconflictlemma слово — удалить конфликтную лемму\n\n' ) # === Управление админами (только для суперадминов) === if is_super_admin: help_text += ( - f'{tg_emoji(4960891456869893259)} Управление админами (только для владельцев):\n' + f'{tg_emoji("4960891456869893259")} Управление админами (только для владельцев):\n' '/addadmin ID — добавить администратора\n' '/remadmin ID — удалить администратора\n' '/redactcomment — изменить комментарий под постом\n' @@ -130,8 +132,8 @@ async def start_cmd(update: Message | CallbackQuery) -> None: # === Типы проверок === help_text += ( - f'{tg_emoji(4961021096162755737)} Типы проверок:\n' - '• Подстрока — простой поиск в тексте\n' + f'{tg_emoji("4961021096162755737")} Типы проверок:\n' + '• Слово — простой поиск в тексте\n' '• Лемма — все формы слова (купить→куплю, купил, купишь...)\n' '• Часть — поиск без пробелов (обходит \"к у п и т ь\")\n' '• Временные — автоматически удаляются через N минут\n' @@ -159,4 +161,4 @@ async def start_cmd(update: Message | CallbackQuery) -> None: log_type="ERROR" ) if is_callback: - await update.answer(f'{tg_emoji(4963277744994518278)} Ошибка отображения справки', show_alert=True) + await update.answer(f'{tg_emoji("4963277744994518278")} Ошибка отображения справки', show_alert=True) diff --git a/bot/handlers/commands/users/stats.py b/bot/handlers/commands/users/stats.py index 51bda9f..e24cfc3 100644 --- a/bot/handlers/commands/users/stats.py +++ b/bot/handlers/commands/users/stats.py @@ -153,7 +153,13 @@ async def stats_cmd(update: Message | CallbackQuery) -> None: conflict_words_count = len(data.get('conflict_substring', set())) conflict_lemmas_count = len(data.get('conflict_lemma', set())) - total_conflict = conflict_words_count + conflict_lemmas_count + conflict_parts_count = len(data.get('conflict_part', set())) + + total_conflict = ( + conflict_words_count + + conflict_lemmas_count + + conflict_parts_count + ) output += f"⚔️ Режим антиконфликта\n" output += f"├─ ⏱ Осталось: {format_time_remaining(time_left_minutes)}\n" diff --git a/bot/handlers/commands/users/word.py b/bot/handlers/commands/users/word.py index 5a5d82a..b3361df 100644 --- a/bot/handlers/commands/users/word.py +++ b/bot/handlers/commands/users/word.py @@ -108,7 +108,7 @@ async def add_word_cmd(message: Message) -> None: try: added = await manager.add_banword( word=word, - word_type=BanWordType.SUBSTRING, + word_type=BanWordType.WORD, added_by=message.from_user.id, reason=f"Добавлено через команду" ) @@ -126,7 +126,7 @@ async def add_word_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления банворда: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления банворда: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -168,7 +168,7 @@ async def add_lemma_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления леммы: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления леммы: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -210,7 +210,7 @@ async def add_part_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления части: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления части: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -246,7 +246,7 @@ async def add_temp_word_cmd(message: Message) -> None: try: added = await manager.add_temp_banword( word=word, - word_type=BanWordType.SUBSTRING, + word_type=BanWordType.WORD, minutes=minutes, added_by=message.from_user.id ) @@ -265,7 +265,7 @@ async def add_temp_word_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления временного банворда: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления временного банворда: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -319,7 +319,7 @@ async def add_temp_lemma_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления временной леммы: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления временной леммы: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -360,7 +360,7 @@ async def add_exception_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка добавления исключения: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка добавления исключения: {e}", log_type="CMD") await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") @@ -384,7 +384,7 @@ async def remove_word_cmd(message: Message) -> None: manager = get_manager() try: - removed = await manager.remove_banword(word=word, word_type=BanWordType.SUBSTRING) + removed = await manager.remove_banword(word=word, word_type=BanWordType.WORD) if removed: text = format_success_message("удалена", word, "подстрока") @@ -394,7 +394,7 @@ async def remove_word_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления банворда: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления банворда: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") @@ -422,7 +422,7 @@ async def remove_lemma_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления леммы: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления леммы: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") @@ -450,7 +450,7 @@ async def remove_part_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления части: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления части: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") @@ -469,7 +469,7 @@ async def remove_temp_word_cmd(message: Message) -> None: manager = get_manager() try: - removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.SUBSTRING) + removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.WORD) if removed: text = format_success_message("удалена", word, "временная подстрока") @@ -479,7 +479,7 @@ async def remove_temp_word_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления временного банворда: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления временного банворда: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") @@ -508,7 +508,7 @@ async def remove_temp_lemma_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления временной леммы: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления временной леммы: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") @@ -536,5 +536,5 @@ async def remove_exception_cmd(message: Message) -> None: await message.answer(text, parse_mode="HTML") except Exception as e: - logger.error(f"Ошибка удаления исключения: {e}", log_type="CMD", exc_info=True) + logger.error(f"Ошибка удаления исключения: {e}", log_type="CMD") await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") diff --git a/bot/handlers/messages/__init__.py b/bot/handlers/messages/__init__.py index 9efe58a..5b60e3e 100644 --- a/bot/handlers/messages/__init__.py +++ b/bot/handlers/messages/__init__.py @@ -1,15 +1,9 @@ from aiogram import Router from .default_msg import router as default_message_router -from .ping_test import router as ping_test_message_router # Настройка экспорта и роутера router: Router = Router(name=__name__) -# Подготовка роутера команд -# router.include_routers( -# ping_test_message_router, -# ) - # Подключение стандартного роутера router.include_router(default_message_router) diff --git a/bot/middlewares/__init__.py b/bot/middlewares/__init__.py index c1db9b0..e25a065 100644 --- a/bot/middlewares/__init__.py +++ b/bot/middlewares/__init__.py @@ -47,8 +47,8 @@ def setup_middlewares( bot: Bot, admin_ids: list[int] = settings.ADMIN_ID+settings.OWNER_ID, channel_ids: list[int | str] | None = None, - enable_spam_check: bool = False, - enable_subscription_check: bool = False, + enable_spam_check: bool = settings.enable_spam_check, + enable_subscription_check: bool = settings.enable_subscription_check, ) -> dict: """ Регистрирует все middleware в диспетчере. @@ -138,5 +138,3 @@ def setup_middlewares( ) return instances - - diff --git a/bot/middlewares/banwords_mdw.py b/bot/middlewares/banwords_mdw.py index 7d4f459..6949822 100644 --- a/bot/middlewares/banwords_mdw.py +++ b/bot/middlewares/banwords_mdw.py @@ -1,13 +1,5 @@ """ Middleware для проверки сообщений на запрещённые слова (банворды). - -✅ ИСПРАВЛЕНО: -- Полная нормализация текста с использованием UNICODE_MAP -- Удаление повторов символов (леееейн → лейн) -- Игнорирование разделителей (л.е.й.н → лейн) -- Поддержка всех типов проверок (SUBSTRING, LEMMA, PART, CONFLICT) -- Белый список и режимы тишины/конфликта -- Нет уведомлений в режиме тишины """ from typing import Callable, Dict, Any, Awaitable, Optional @@ -15,7 +7,7 @@ import re import unicodedata from aiogram import BaseMiddleware -from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import Message from aiogram.exceptions import TelegramBadRequest from configs import settings, UNICODE_MAP, LATIN_TO_CYRILLIC, CYRILLIC_NORMALIZE @@ -23,97 +15,86 @@ from database import get_manager, BanWordType from bot.special import extract_words, get_lemma from middleware.loggers import logger -__all__ = ("BanWordsMiddleware",) +__all__ = ("BanWordsMiddleware",) +URL_PATTERN = re.compile( + r'(https?://\S+|www\.\S+)', + re.IGNORECASE +) class TextNormalizer: """ Класс для многоступенчатой нормализации текста. - Приводит различные юникод-символы к базовым буквам, - удаляет повторы, убирает разделители. """ - # Объединяем все словари замен в один - FULL_MAP = {} + FULL_MAP: Dict[str, str] = {} 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 = [] + result: list[str] = [] for ch in text: - # Сначала пробуем заменить по карте - if ch in cls.FULL_MAP: - result.append(cls.FULL_MAP[ch]) - else: - result.append(ch) - # Приводим к нижнему регистру после замен (чтобы избежать потери регистра в карте) + result.append(cls.FULL_MAP.get(ch, 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: - def repl(m): - ch = m.group(1) - return ch # вместо ch * 2 — теперь схлопываем до одного символа - + def collapse_repeats(cls, text: str) -> str: + def repl(match: re.Match[str]) -> str: + return match.group(1) 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 разлагает символы типа "ё" в "е" + умляут, но нам лучше оставить как есть, - # т.к. у нас есть прямые замены. Однако для совместимости применим. + def normalize_full( + cls, + text: str, + remove_sep: bool = True, + collapse: bool = True + ) -> str: 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: + def normalize_for_part_token(cls, text: str) -> str: """ - Нормализация для типа PART: - - Полная нормализация - - Удаление всех не-буквенных символов (кроме пробелов) - - Приведение к нижнему регистру + Нормализация для PART: + - NFKC + - lower() + - удаление zero-width + - схлопывание повторов латиницы + - БЕЗ LATIN_TO_CYRILLIC """ - 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() + text = unicodedata.normalize('NFKC', text) + text = text.lower() + + # удаляем zero-width + text = re.sub(r'[\u200B-\u200D\uFEFF]', '', text) + + # схлопываем повторы букв (3+ → 1) + text = re.sub(r'([a-z])\1+', r'\1', text) + + return text class BanWordsMiddleware(BaseMiddleware): - def __init__(self): + + def __init__(self) -> None: super().__init__() self.manager = get_manager() self.normalizer = TextNormalizer() @@ -124,219 +105,178 @@ class BanWordsMiddleware(BaseMiddleware): event: Message, data: Dict[str, Any] ) -> Any: - # Проверяем наличие текста или подписи + if not event.text and not event.caption: return await handler(event, data) - message_text = event.text or event.caption + message_text: str = 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) + user_id: int = event.from_user.id + is_super_admin: bool = user_id in settings.OWNER_ID + is_admin: bool = 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 # Сообщение удалено, дальше не обрабатываем + await self._handle_spam(event) + return None return await handler(event, data) - async def _check_message(self, text: str) -> Optional[Dict[str, str]]: - """ - Многоступенчатая проверка текста. - Возвращает словарь с причиной блокировки или None. - """ - # 1. Повторяющиеся символы (например, "леееейн") — блокируем сразу - # repeat_result = self._check_repeated_chars(text) - # if repeat_result: - # return repeat_result + @staticmethod + def is_allowed_url(url: str, allowed: str) -> bool: + url_lower = url.lower() + allowed_lower = allowed.lower() + if allowed_lower.endswith('/'): + # исключение со слешем: только строгое начало с этим слешем + return url_lower.startswith(allowed_lower) + else: + # исключение без слеша: разрешаем точное совпадение или начало с добавлением слеша + return url_lower == allowed_lower or url_lower.startswith(allowed_lower + '/') - # 2. Получаем кэшированные списки - substring_words = self.manager.get_banwords_cached(BanWordType.SUBSTRING) + async def _check_message(self, text: str) -> Optional[Dict[str, str]]: + whitelist = { + w.lower().strip() + for w in self.manager.get_whitelist_cached() + } + + # ================= URL CHECK ================= + urls = URL_PATTERN.findall(text) + + for url in urls: + url_lower = url.lower() + + # если URL начинается с разрешённого исключения — пропускаем + if any(self.is_allowed_url(url_lower, allowed) for allowed in whitelist): + continue + + # если нет разрешения — проверяем WORD-правила для URL + for word in self.manager.get_banwords_cached(BanWordType.WORD): + if word in url_lower: + return {"word": word, "type": "word"} + # ============================================= + + # 2. Убираем URL из текста для word/lemma проверки + text_without_urls = URL_PATTERN.sub(' ', text) + + word_words = self.manager.get_banwords_cached(BanWordType.WORD) lemma_words = self.manager.get_banwords_cached(BanWordType.LEMMA) part_words = self.manager.get_banwords_cached(BanWordType.PART) - conflict_substring = self.manager.get_banwords_cached(BanWordType.CONFLICT_SUBSTRING) + conflict_word = self.manager.get_banwords_cached(BanWordType.CONFLICT_WORD) conflict_lemma = self.manager.get_banwords_cached(BanWordType.CONFLICT_LEMMA) + conflict_part = self.manager.get_banwords_cached(BanWordType.CONFLICT_PART) - # 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"} + normalized_text = self.normalizer.normalize_full(text) + + for word in conflict_word: + if self.normalizer.normalize_full(word) in normalized_text: + return {"word": word, "type": "conflict_word"} - # 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"} + if get_lemma(word_text) in conflict_lemma: + return {"word": word_text, "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"} + # WORD — строгое совпадение как отдельное слово + for word in word_words: + pattern = r'(? Optional[Dict[str, str]]: - """ - Проверяет на наличие 3+ повторяющихся букв подряд. - Использует сырой текст без нормализации (чтобы поймать "леееейн"). - """ - # Ищем повторения букв (только кириллица/латиница) - pattern = re.compile(r'([а-яёa-zA-Z])\1{2,}', re.IGNORECASE) - matches = pattern.finditer(text) - 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 + @staticmethod + async def _handle_spam( + message: Message, + ) -> None: - async def _handle_spam(self, message: Message, spam_result: Dict[str, str]) -> None: - """Обрабатывает спам-сообщение: удаляет, логирует, уведомляет (кроме silence)""" - user = message.from_user - matched_word = spam_result["word"] - match_type = spam_result["type"] - message_text = message.text or message.caption or "[нет текста]" - - # В режиме тишины удаляем молча - 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 - - # Удаляем сообщение 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") + logger.info(f"Удалено сообщение: {message.text}") + except TelegramBadRequest: return - - # Логируем в БД - await self.manager.log_spam( - user_id=user.id, - username=user.username or f"id{user.id}", - chat_id=message.chat.id, - message_text=message_text, - matched_word=matched_word, - match_type=match_type - ) - - # Уведомляем админов - 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 -) -> None: - """Отправляет уведомление об удалении в админ-чат (берёт ID из БД)""" - 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"🚫 Удалено сообщение\n\n" - f"👤 Пользователь: {username}\n" - f"🆔 ID: {user.id}\n" - f"📊 Нарушений: {spam_count}\n\n" - f"💬 Чат: {self._escape_html(chat_title)}\n" - f"🆔 Chat ID: {message.chat.id}\n" - f"{'📌 Topic ID: ' + str(source_thread_id) + '\n' if source_thread_id else ''}" - f"🔗 Message ID: {message.message_id}\n\n" - f"🔍 Триггер: {self._escape_html(matched_word)}\n" - f"📝 Тип: {self._get_type_emoji(match_type)} {self._escape_html(match_type)}\n\n" - f"💬 Текст:\n{self._escape_html(message_text[:500])}" - ) - - 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_stats:{user.id}")] - ]) - - try: - # ✅ Получаем настройки из БД (динамические, установленные через /settings) - admin_chat_id = await self.manager.get_bot_setting("admin_chat_id") - admin_thread_id = await self.manager.get_bot_setting("admin_thread_id") - - if admin_chat_id: - await message.bot.send_message( - chat_id=int(admin_chat_id), - text=notification_text, - reply_markup=keyboard, - parse_mode="HTML", - message_thread_id=int(admin_thread_id) if admin_thread_id else None - ) - except Exception as e: - logger.error(f"❌ Уведомление админам: {e}", log_type="BANWORDS") - - @staticmethod - def _get_type_emoji(match_type: str) -> str: - return { - "substring": "🔤", - "lemma": "📖", - "part": "🧩", - "silence": "🔇", - "conflict_substring": "⚔️", - "conflict_lemma": "⚔️", - "repeated_chars": "🔁" - }.get(match_type, "❓") - - @staticmethod - def _escape_html(text: str) -> str: - return str(text).replace("&", "&").replace("<", "<").replace(">", ">") diff --git a/bot/middlewares/spam_mdw.py b/bot/middlewares/spam_mdw.py index b7eb79c..f27d435 100644 --- a/bot/middlewares/spam_mdw.py +++ b/bot/middlewares/spam_mdw.py @@ -105,7 +105,7 @@ class UserSpamStats: self.total_blocks += 1 self.reputation = max(0.5, self.reputation - 0.3) - def detect_spam_patterns(self, time_window: float = 10.0) -> Dict[str, Any]: + def detect_spam_patterns(self, time_window: float = 2.0) -> Dict[str, Any]: """ Умная детекция спама на основе паттернов. УЛУЧШЕНО: учитывает скорость отправки сообщений. @@ -120,7 +120,7 @@ class UserSpamStats: current_time = time() # 1. КРИТИЧНО: Экстремально быстрая отправка (флуд-бот) - very_recent = [ctx for ctx in recent_contexts if (current_time - ctx.timestamp) < 2.0] + very_recent = [ctx for ctx in recent_contexts if (current_time - ctx.timestamp) < time_window] if len(very_recent) >= 5: return { 'is_spam': True, @@ -133,7 +133,7 @@ class UserSpamStats: # 2. КРИТИЧНО: 8+ сообщений за 5 секунд => агрессивный флуд recent_5s = [ctx for ctx in recent_contexts if (current_time - ctx.timestamp) < 5.0] - if len(recent_5s) >= 8: + if len(recent_5s) >= 15: return { 'is_spam': True, 'reason': 'aggressive_flood', @@ -145,7 +145,7 @@ class UserSpamStats: # 3. Медиа-флуд media_contexts = [ctx for ctx in recent_contexts if ctx.media_type] - if len(media_contexts) >= 7: + if len(media_contexts) >= 15: media_recent = [ctx for ctx in media_contexts if (current_time - ctx.timestamp) < 5.0] if len(media_recent) >= 6: return { @@ -303,7 +303,8 @@ class AntiSpamMiddleware(BaseMiddleware): self.enable_reputation = enable_reputation self.log_all = log_all - def _extract_context(self, event: TelegramObject) -> MessageContext: + @staticmethod + def _extract_context(event: TelegramObject) -> MessageContext: """Извлекает контекст из события""" context = MessageContext() diff --git a/bot/templates/message_callback.py b/bot/templates/message_callback.py index 6cba0d7..01358ea 100644 --- a/bot/templates/message_callback.py +++ b/bot/templates/message_callback.py @@ -246,23 +246,40 @@ async def msg( keyboard = markups(markup) try: - # Попытка редактирования (для callback) if edit_if_possible and isinstance(update, CallbackQuery): - sent_message = await message.edit_text( + try: + sent_message = await message.edit_text( + text=text, + reply_markup=keyboard, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview + ) + + if log: + logger.debug( + f"Сообщение отредактировано: {message.message_id}", + log_type='MESSAGE' + ) + + except (TelegramBadRequest, TelegramForbiddenError): + sent_message = await message.answer( + text=text, + reply_markup=keyboard, + parse_mode=parse_mode, + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + protect_content=protect_content + ) + else: + sent_message = await message.answer( text=text, reply_markup=keyboard, parse_mode=parse_mode, - disable_web_page_preview=disable_web_page_preview + disable_web_page_preview=disable_web_page_preview, + disable_notification=disable_notification, + protect_content=protect_content ) - if log: - logger.debug( - f"Сообщение отредактировано: {message.message_id}", - log_type='MESSAGE' - ) - else: - raise TelegramBadRequest - except (TelegramBadRequest, TelegramForbiddenError): # Отправка нового сообщения try: diff --git a/configs/config.py b/configs/config.py index 6eb0423..fd3d987 100644 --- a/configs/config.py +++ b/configs/config.py @@ -45,7 +45,6 @@ class _Settings(BaseSettings): WEBHOOK_URL: Optional[str] = None WEBAPP_HOST: str = "0.0.0.0" WEBAPP_PORT: int = 3131 - LOG_LEVEL: str = "warning" ACCES_LOG: bool = False # API ключи @@ -137,7 +136,10 @@ class _Settings(BaseSettings): SHOW_CAPTION_ABOVE_MEDIA: bool = False # улучшения - ANTI_SPAM: bool = True + ANTI_SPAM: bool = False + enable_spam_check: bool = False + enable_subscription_check: bool = False + BOT_USERNAME: str = "@OvhdLayla2_bot" # ================= ВАЛИДАТОРЫ ================= @field_validator('PARSE_MODE') diff --git a/database/manager.py b/database/manager.py index f29b405..485f8a9 100644 --- a/database/manager.py +++ b/database/manager.py @@ -44,6 +44,7 @@ class BanWordsManager: """Инициализирует базу данных и загружает кэш""" await self.db.init() await self.init_default_bot_settings() # ← добавлено + await self.init_default_words() await self.refresh_cache() logger.info("BanWordsManager инициализирован", log_type="DATABASE") @@ -452,12 +453,13 @@ class BanWordsManager: admins = await self.repo.get_admins() return { - 'substring': banwords.get(BanWordType.SUBSTRING, set()), + 'word': banwords.get(BanWordType.WORD, set()), 'lemma': banwords.get(BanWordType.LEMMA, set()), 'part': banwords.get(BanWordType.PART, set()), - 'conflict_substring': banwords.get(BanWordType.CONFLICT_SUBSTRING, set()), + 'conflict_word': banwords.get(BanWordType.CONFLICT_WORD, set()), 'conflict_lemma': banwords.get(BanWordType.CONFLICT_LEMMA, set()), - 'temp_substring': temp_banwords.get(BanWordType.SUBSTRING, set()), + 'conflict_part': banwords.get(BanWordType.CONFLICT_PART, set()), + 'temp_word': temp_banwords.get(BanWordType.WORD, set()), 'temp_lemma': temp_banwords.get(BanWordType.LEMMA, set()), 'whitelist': whitelist, 'admins': admins @@ -516,6 +518,50 @@ class BanWordsManager: ) return [] + async def init_default_words(self) -> None: + """ + Добавляет базовые банворды и whitelist при первом запуске. + Ничего не перезаписывает, если уже существует. + """ + try: + from configs import settings + + # --- Базовые слова --- + default_word = {"http", "t.me/"} + default_part = {"bot"} + default_lemma = {"скам", "мошенник"} + + # Проверяем уже существующие + existing = await self.repo.get_all_banwords() + + # word + for word in default_word: + if word not in existing.get(BanWordType.WORD, set()): + await self.repo.add_banword(word, BanWordType.WORD) + + # PART + for word in default_part: + if word not in existing.get(BanWordType.PART, set()): + await self.repo.add_banword(word, BanWordType.PART) + + # LEMMA + for word in default_lemma: + if word not in existing.get(BanWordType.LEMMA, set()): + await self.repo.add_banword(word, BanWordType.LEMMA) + + # --- Добавляем username бота в whitelist --- + bot_username = settings.BOT_USERNAME + + if bot_username: + whitelist = await self.repo.get_whitelist() + if bot_username.lower() not in whitelist: + await self.repo.add_whitelist(bot_username.lower()) + + logger.info("Базовые слова и whitelist инициализированы", log_type="DATABASE") + + except Exception as e: + logger.error(f"Ошибка инициализации базовых слов: {e}", log_type="DATABASE") + async def cleanup_expired_temp_words(self) -> int: """ Удаляет истёкшие временные банворды. diff --git a/database/models.py b/database/models.py index c56bab3..3a761d4 100644 --- a/database/models.py +++ b/database/models.py @@ -31,11 +31,12 @@ class Base(DeclarativeBase): class BanWordType(str, PyEnum): """Типы банвордов""" - SUBSTRING = "substring" + WORD = "word" LEMMA = "lemma" PART = "part" - CONFLICT_SUBSTRING = "conflict_substring" + CONFLICT_WORD = "conflict_word" CONFLICT_LEMMA = "conflict_lemma" + CONFLICT_PART = "conflict_part" class SpamMode(str, PyEnum): @@ -62,7 +63,11 @@ class BanWord(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) word: Mapped[str] = mapped_column(String(255), nullable=False, index=True) type: Mapped[BanWordType] = mapped_column( - Enum(BanWordType, native_enum=False), + Enum( + BanWordType, + native_enum=False, + values_callable=lambda enum: [e.value for e in enum] + ), nullable=False, index=True ) @@ -95,8 +100,13 @@ class TempBanWord(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) word: Mapped[str] = mapped_column(String(255), nullable=False, index=True) type: Mapped[BanWordType] = mapped_column( - Enum(BanWordType, native_enum=False), - nullable=False + Enum( + BanWordType, + native_enum=False, + values_callable=lambda enum: [e.value for e in enum] + ), + nullable=False, + index=True ) added_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) added_at: Mapped[datetime] = mapped_column( diff --git a/database/repository.py b/database/repository.py index 51fa523..489c258 100644 --- a/database/repository.py +++ b/database/repository.py @@ -158,11 +158,12 @@ class BanWordsRepository: async def get_all_banwords(self) -> dict[BanWordType, Set[str]]: result = { - BanWordType.SUBSTRING: set(), + BanWordType.WORD: set(), BanWordType.LEMMA: set(), BanWordType.PART: set(), - BanWordType.CONFLICT_SUBSTRING: set(), + BanWordType.CONFLICT_WORD: set(), BanWordType.CONFLICT_LEMMA: set(), + BanWordType.CONFLICT_PART: set(), } try: async with self.db.get_session() as session: @@ -335,7 +336,7 @@ class BanWordsRepository: async def get_all_temp_banwords(self) -> dict[BanWordType, Set[str]]: """Получает все активные временные банворды по типам""" result = { - BanWordType.SUBSTRING: set(), + BanWordType.WORD: set(), BanWordType.LEMMA: set(), }