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(),
}