forked from Muzifs/LGBot
326 lines
13 KiB
Python
326 lines
13 KiB
Python
# Система управления бранными словами
|
||
# Список слов хранится в JSON файле для возможности управления через команды
|
||
|
||
import json
|
||
import os
|
||
import logging
|
||
import re
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Путь к файлу с бранными словами
|
||
BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words.json')
|
||
|
||
# Кэш для загруженных слов
|
||
_bad_words_cache = None
|
||
_exceptions_cache = None
|
||
_whole_word_patterns_cache = None
|
||
_contains_patterns_cache = None
|
||
|
||
def load_bad_words():
|
||
"""
|
||
Загружает список бранных слов из JSON файла.
|
||
Поддерживает как новый формат (patterns), так и старый (bad_words).
|
||
|
||
Returns:
|
||
tuple: (список бранных слов, список исключений)
|
||
"""
|
||
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _contains_patterns_cache
|
||
|
||
try:
|
||
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
# Проверяем, какой формат используется
|
||
if 'patterns' in data:
|
||
# Новый формат с паттернами
|
||
patterns = data['patterns']
|
||
_whole_word_patterns_cache = patterns.get('whole_word', [])
|
||
_contains_patterns_cache = patterns.get('contains', [])
|
||
_exceptions_cache = data.get('exceptions', [])
|
||
|
||
# Для обратной совместимости объединяем в один список
|
||
_bad_words_cache = _whole_word_patterns_cache + _contains_patterns_cache
|
||
|
||
logger.info(
|
||
f"Загружено паттернов: {len(_whole_word_patterns_cache)} whole_word, "
|
||
f"{len(_contains_patterns_cache)} contains, {len(_exceptions_cache)} исключений"
|
||
)
|
||
else:
|
||
# Старый формат для обратной совместимости
|
||
_bad_words_cache = data.get('bad_words', [])
|
||
_exceptions_cache = data.get('exceptions', [])
|
||
_whole_word_patterns_cache = []
|
||
_contains_patterns_cache = _bad_words_cache.copy()
|
||
|
||
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов (старый формат) и {len(_exceptions_cache)} исключений")
|
||
|
||
return _bad_words_cache, _exceptions_cache
|
||
except FileNotFoundError:
|
||
logger.error(f"Файл {BAD_WORDS_FILE} не найден")
|
||
return [], []
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"Ошибка чтения JSON: {e}")
|
||
return [], []
|
||
|
||
def save_bad_words(bad_words: list, exceptions: list):
|
||
"""
|
||
Сохраняет список бранных слов в JSON файл.
|
||
|
||
Args:
|
||
bad_words: Список бранных слов
|
||
exceptions: Список исключений
|
||
|
||
Returns:
|
||
bool: True если успешно, иначе False
|
||
"""
|
||
global _bad_words_cache, _exceptions_cache
|
||
|
||
try:
|
||
# Создаем директорию, если её нет
|
||
os.makedirs(os.path.dirname(BAD_WORDS_FILE), exist_ok=True)
|
||
|
||
data = {
|
||
'bad_words': sorted(list(set(bad_words))), # Убираем дубликаты и сортируем
|
||
'exceptions': sorted(list(set(exceptions)))
|
||
}
|
||
|
||
with open(BAD_WORDS_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||
|
||
# Обновляем кэш
|
||
_bad_words_cache = data['bad_words']
|
||
_exceptions_cache = data['exceptions']
|
||
|
||
logger.info(f"Сохранено {len(_bad_words_cache)} бранных слов и {len(_exceptions_cache)} исключений")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка сохранения: {e}")
|
||
return False
|
||
|
||
def get_bad_words():
|
||
"""Возвращает список бранных слов (с кэшированием)"""
|
||
global _bad_words_cache
|
||
if _bad_words_cache is None:
|
||
load_bad_words()
|
||
return _bad_words_cache or []
|
||
|
||
def get_exceptions():
|
||
"""Возвращает список исключений (с кэшированием)"""
|
||
global _exceptions_cache
|
||
if _exceptions_cache is None:
|
||
load_bad_words()
|
||
return _exceptions_cache or []
|
||
|
||
def reload_words():
|
||
"""Перезагружает списки из файла (сбрасывает кэш)"""
|
||
global _bad_words_cache, _exceptions_cache
|
||
_bad_words_cache = None
|
||
_exceptions_cache = None
|
||
return load_bad_words()
|
||
|
||
def normalize_text(text: str) -> str:
|
||
"""
|
||
Нормализует текст для обхода обфускации.
|
||
|
||
Убирает:
|
||
- Звездочки, точки, подчеркивания между буквами (х*й, х.у.й, х_у_й → хуй)
|
||
- Повторяющиеся символы (хууууууй → хуй)
|
||
- ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (х у й → хуй, но "не бу" остаётся "не бу")
|
||
|
||
Args:
|
||
text: Исходный текст
|
||
|
||
Returns:
|
||
Нормализованный текст
|
||
"""
|
||
if not text:
|
||
return text
|
||
|
||
# Приводим к нижнему регистру
|
||
normalized = text.lower()
|
||
|
||
# Циклически убираем обфускацию, пока что-то меняется
|
||
max_iterations = 10
|
||
for _ in range(max_iterations):
|
||
before = normalized
|
||
|
||
# Убираем звёздочки, точки, дефисы, подчёркивания между буквами
|
||
# х*й, х.у.й, х_у_й → хуй
|
||
normalized = re.sub(r'([а-яё])[\*\.\-_]+([а-яё])', r'\1\2', normalized)
|
||
|
||
# Убираем ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (обфускация)
|
||
# "х у й" → "хуй", но "не бу" → "не бу" (не склеиваем обычные слова)
|
||
# Паттерн: одиночная буква + пробелы + одиночная буква
|
||
normalized = re.sub(r'\b([а-яё])\s+([а-яё])\b', r'\1\2', normalized)
|
||
|
||
if before == normalized:
|
||
break
|
||
|
||
# Убираем повторяющиеся буквы (более 2 подряд)
|
||
# хууууууй → хуй, пииииздец → пиздец
|
||
normalized = re.sub(r'([а-яё])\1{2,}', r'\1', normalized)
|
||
|
||
return normalized
|
||
|
||
# Предзагружаем слова при импорте модуля в кэш
|
||
load_bad_words()
|
||
|
||
def contains_bad_word(text: str) -> bool:
|
||
"""
|
||
Проверяет, содержит ли текст бранные слова.
|
||
|
||
Использует:
|
||
- Нормализацию текста для обхода обфускации
|
||
- Проверку границ слов для whole_word паттернов
|
||
- Проверку подстрок для contains паттернов
|
||
- Список исключений
|
||
|
||
Args:
|
||
text: Текст для проверки
|
||
|
||
Returns:
|
||
True, если найдено бранное слово, иначе False
|
||
"""
|
||
if not text:
|
||
return False
|
||
|
||
# Нормализуем текст (обход обфускации)
|
||
normalized_text = normalize_text(text)
|
||
|
||
# Получаем паттерны и исключения
|
||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||
|
||
# Если кэш не загружен, загружаем
|
||
if _whole_word_patterns_cache is None:
|
||
load_bad_words()
|
||
|
||
whole_word_patterns = _whole_word_patterns_cache or []
|
||
contains_patterns = _contains_patterns_cache or []
|
||
exceptions = _exceptions_cache or []
|
||
|
||
# 1. Проверяем whole_word паттерны (только целые слова)
|
||
for pattern in whole_word_patterns:
|
||
# Используем границы слов \b для поиска только целых слов
|
||
regex = r'\b' + re.escape(pattern) + r'\b'
|
||
if re.search(regex, normalized_text, re.IGNORECASE):
|
||
# Проверяем, не входит ли в исключения
|
||
is_exception = False
|
||
for exception in exceptions:
|
||
if exception in normalized_text and pattern in exception:
|
||
is_exception = True
|
||
break
|
||
|
||
if not is_exception:
|
||
return True
|
||
|
||
# 2. Проверяем contains паттерны (любое вхождение)
|
||
for pattern in contains_patterns:
|
||
if pattern in normalized_text:
|
||
# Проверяем все вхождения паттерна
|
||
start = 0
|
||
while True:
|
||
pos = normalized_text.find(pattern, start)
|
||
if pos == -1:
|
||
break
|
||
|
||
# Проверяем, не входит ли в исключение
|
||
is_exception = False
|
||
for exception in exceptions:
|
||
if pattern in exception:
|
||
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
|
||
if exc_start != -1:
|
||
exc_end = exc_start + len(exception)
|
||
if exc_start <= pos < exc_end:
|
||
is_exception = True
|
||
break
|
||
|
||
if not is_exception:
|
||
return True
|
||
|
||
start = pos + 1
|
||
|
||
return False
|
||
|
||
def get_bad_words_from_text(text: str) -> list:
|
||
"""
|
||
Возвращает список найденных бранных слов в тексте.
|
||
|
||
Использует:
|
||
- Нормализацию текста для обхода обфускации
|
||
- Проверку границ слов для whole_word паттернов
|
||
- Проверку подстрок для contains паттернов
|
||
- Список исключений
|
||
|
||
Args:
|
||
text: Текст для проверки
|
||
|
||
Returns:
|
||
Список найденных бранных слов
|
||
"""
|
||
if not text:
|
||
return []
|
||
|
||
# Нормализуем текст (обход обфускации)
|
||
normalized_text = normalize_text(text)
|
||
found_words = []
|
||
|
||
# Получаем паттерны и исключения
|
||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||
|
||
# Если кэш не загружен, загружаем
|
||
if _whole_word_patterns_cache is None:
|
||
load_bad_words()
|
||
|
||
whole_word_patterns = _whole_word_patterns_cache or []
|
||
contains_patterns = _contains_patterns_cache or []
|
||
exceptions = _exceptions_cache or []
|
||
|
||
# 1. Проверяем whole_word паттерны (только целые слова)
|
||
for pattern in whole_word_patterns:
|
||
# Используем границы слов \b для поиска только целых слов
|
||
regex = r'\b' + re.escape(pattern) + r'\b'
|
||
if re.search(regex, normalized_text, re.IGNORECASE):
|
||
# Проверяем, не входит ли в исключения
|
||
is_exception = False
|
||
for exception in exceptions:
|
||
if exception in normalized_text and pattern in exception:
|
||
is_exception = True
|
||
break
|
||
|
||
if not is_exception and pattern not in found_words:
|
||
found_words.append(pattern)
|
||
|
||
# 2. Проверяем contains паттерны (любое вхождение)
|
||
for pattern in contains_patterns:
|
||
if pattern in normalized_text:
|
||
# Проверяем все вхождения паттерна
|
||
start = 0
|
||
word_is_valid = False
|
||
while True:
|
||
pos = normalized_text.find(pattern, start)
|
||
if pos == -1:
|
||
break
|
||
|
||
# Проверяем, не входит ли в исключение
|
||
is_exception = False
|
||
for exception in exceptions:
|
||
if pattern in exception:
|
||
exc_start = normalized_text.find(exception, max(0, pos - len(exception)))
|
||
if exc_start != -1:
|
||
exc_end = exc_start + len(exception)
|
||
if exc_start <= pos < exc_end:
|
||
is_exception = True
|
||
break
|
||
|
||
if not is_exception:
|
||
word_is_valid = True
|
||
break
|
||
|
||
start = pos + 1
|
||
|
||
# Добавляем слово только если оно действительно найдено (не в исключении)
|
||
if word_is_valid and pattern not in found_words:
|
||
found_words.append(pattern)
|
||
|
||
return found_words |