forked from Muzifs/LGBot
Улучшение системы фильтрации мата и управления кармой
Фильтрация бранных слов:
- Добавлена полная транслитерация латиница→кириллица (все 26 букв)
- Добавлен маппинг цифр→буквы (0→о, 3→з, 4→ч, 6→б, 8→в и др.)
- Добавлено определение ASCII-art паттернов (><→х, }|{→ж и др.)
- Реализована 3-уровневая система категоризации паттернов:
* whole_word - только целые слова с границами \b
* word_start - только начало слова (новая категория)
* contains - любое вхождение (минимум 4 буквы)
- Добавлен расширенный список исключений для предотвращения ложных срабатываний
(требует, употреблять, скребу, гребу, республика, документ и др.)
Система кармы:
- Добавлен штраф за эмодзи клоуна 🤡 (-20 кармы тому, кто добавил)
- Карма не возвращается при снятии клоуна
- Реализована возможность отключения кармы в конкретных топиках:
* Новая таблица disabled_karma_topics в БД
* Методы: is_karma_disabled_in_topic(), disable/enable_karma_in_topic()
- Перенесены команды управления кармой из /badwords в /karma:
* /karma disable - отключить карму в текущем топике
* /karma enable - включить карму в текущем топике
* /karma status - показать статус кармы во всех топиках
- Убраны упоминания пользователей в команде /top (используется <code>)
Аудит и логирование:
- Добавлена отправка в канал аудита для операций со словами:
* Добавление/удаление бранных слов
* Добавление/удаление исключений
- Расширен action_reporter для поддержки операций без привязки к пользователю
This commit is contained in:
@@ -59,23 +59,27 @@ class ActionReporter:
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
# Отправляет лог действия в админ-чат
|
# Отправляет лог действия в админ-чат
|
||||||
async def log_action(self, action: str, user_id: int, admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None):
|
async def log_action(self, action: str, user_id: Optional[int], admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# Получаем информацию о пользователе и администраторе
|
# Получаем информацию о пользователе и администраторе
|
||||||
user_info = await self._get_user_info(user_id)
|
|
||||||
admin_info = await self._get_admin_info(admin_id)
|
admin_info = await self._get_admin_info(admin_id)
|
||||||
|
|
||||||
# Формируем сообщение
|
# Формируем сообщение
|
||||||
msg = f"⚙️ <b>Действие:</b> {action}\n"
|
msg = f"⚙️ <b>Действие:</b> {action}\n"
|
||||||
|
|
||||||
if duration:
|
if duration:
|
||||||
msg += f"⏱ <b>Длительность:</b> {duration}\n"
|
msg += f"⏱ <b>Длительность:</b> {duration}\n"
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
msg += f"📝 <b>Причина:</b> <i>{reason}</i>\n"
|
msg += f"📝 <b>Причина:</b> <i>{reason}</i>\n"
|
||||||
|
|
||||||
msg += f"\n{user_info}\n\n{admin_info}"
|
# Добавляем информацию о пользователе, если указан
|
||||||
|
if user_id is not None:
|
||||||
|
user_info = await self._get_user_info(user_id)
|
||||||
|
msg += f"\n{user_info}\n\n{admin_info}"
|
||||||
|
else:
|
||||||
|
msg += f"\n{admin_info}"
|
||||||
|
|
||||||
# Отправляем лог с изображением
|
# Отправляем лог с изображением
|
||||||
if photo_path and os.path.exists(photo_path):
|
if photo_path and os.path.exists(photo_path):
|
||||||
|
|||||||
240
src/bad_words.py
240
src/bad_words.py
@@ -15,6 +15,7 @@ BAD_WORDS_FILE = os.path.join(os.path.dirname(__file__), 'data', 'bad_words.json
|
|||||||
_bad_words_cache = None
|
_bad_words_cache = None
|
||||||
_exceptions_cache = None
|
_exceptions_cache = None
|
||||||
_whole_word_patterns_cache = None
|
_whole_word_patterns_cache = None
|
||||||
|
_word_start_patterns_cache = None
|
||||||
_contains_patterns_cache = None
|
_contains_patterns_cache = None
|
||||||
|
|
||||||
def load_bad_words():
|
def load_bad_words():
|
||||||
@@ -25,7 +26,7 @@ def load_bad_words():
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: (список бранных слов, список исключений)
|
tuple: (список бранных слов, список исключений)
|
||||||
"""
|
"""
|
||||||
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _contains_patterns_cache
|
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
|
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
|
||||||
@@ -36,14 +37,16 @@ def load_bad_words():
|
|||||||
# Новый формат с паттернами
|
# Новый формат с паттернами
|
||||||
patterns = data['patterns']
|
patterns = data['patterns']
|
||||||
_whole_word_patterns_cache = patterns.get('whole_word', [])
|
_whole_word_patterns_cache = patterns.get('whole_word', [])
|
||||||
|
_word_start_patterns_cache = patterns.get('word_start', [])
|
||||||
_contains_patterns_cache = patterns.get('contains', [])
|
_contains_patterns_cache = patterns.get('contains', [])
|
||||||
_exceptions_cache = data.get('exceptions', [])
|
_exceptions_cache = data.get('exceptions', [])
|
||||||
|
|
||||||
# Для обратной совместимости объединяем в один список
|
# Для обратной совместимости объединяем в один список
|
||||||
_bad_words_cache = _whole_word_patterns_cache + _contains_patterns_cache
|
_bad_words_cache = _whole_word_patterns_cache + _word_start_patterns_cache + _contains_patterns_cache
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Загружено паттернов: {len(_whole_word_patterns_cache)} whole_word, "
|
f"Загружено паттернов: {len(_whole_word_patterns_cache)} whole_word, "
|
||||||
|
f"{len(_word_start_patterns_cache)} word_start, "
|
||||||
f"{len(_contains_patterns_cache)} contains, {len(_exceptions_cache)} исключений"
|
f"{len(_contains_patterns_cache)} contains, {len(_exceptions_cache)} исключений"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -51,6 +54,7 @@ def load_bad_words():
|
|||||||
_bad_words_cache = data.get('bad_words', [])
|
_bad_words_cache = data.get('bad_words', [])
|
||||||
_exceptions_cache = data.get('exceptions', [])
|
_exceptions_cache = data.get('exceptions', [])
|
||||||
_whole_word_patterns_cache = []
|
_whole_word_patterns_cache = []
|
||||||
|
_word_start_patterns_cache = []
|
||||||
_contains_patterns_cache = _bad_words_cache.copy()
|
_contains_patterns_cache = _bad_words_cache.copy()
|
||||||
|
|
||||||
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов (старый формат) и {len(_exceptions_cache)} исключений")
|
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов (старый формат) и {len(_exceptions_cache)} исключений")
|
||||||
@@ -114,9 +118,12 @@ def get_exceptions():
|
|||||||
|
|
||||||
def reload_words():
|
def reload_words():
|
||||||
"""Перезагружает списки из файла (сбрасывает кэш)"""
|
"""Перезагружает списки из файла (сбрасывает кэш)"""
|
||||||
global _bad_words_cache, _exceptions_cache
|
global _bad_words_cache, _exceptions_cache, _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache
|
||||||
_bad_words_cache = None
|
_bad_words_cache = None
|
||||||
_exceptions_cache = None
|
_exceptions_cache = None
|
||||||
|
_whole_word_patterns_cache = None
|
||||||
|
_word_start_patterns_cache = None
|
||||||
|
_contains_patterns_cache = None
|
||||||
return load_bad_words()
|
return load_bad_words()
|
||||||
|
|
||||||
def normalize_text(text: str) -> str:
|
def normalize_text(text: str) -> str:
|
||||||
@@ -124,6 +131,10 @@ def normalize_text(text: str) -> str:
|
|||||||
Нормализует текст для обхода обфускации.
|
Нормализует текст для обхода обфускации.
|
||||||
|
|
||||||
Убирает:
|
Убирает:
|
||||||
|
- Транслитерацию латиницы в кириллицу (xyu → хуй, p0rn → porn)
|
||||||
|
- Цифры похожие на буквы (0→о, 3→з, 6→б)
|
||||||
|
- Языковые варианты (ё→е)
|
||||||
|
- ASCII-имитации (><→х, |\|→и, /\→л)
|
||||||
- Звездочки, точки, подчеркивания между буквами (х*й, х.у.й, х_у_й → хуй)
|
- Звездочки, точки, подчеркивания между буквами (х*й, х.у.й, х_у_й → хуй)
|
||||||
- Повторяющиеся символы (хууууууй → хуй)
|
- Повторяющиеся символы (хууууууй → хуй)
|
||||||
- ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (х у й → хуй, но "не бу" остаётся "не бу")
|
- ОДИНОЧНЫЕ пробелы между ОДИНОЧНЫМИ буквами (х у й → хуй, но "не бу" остаётся "не бу")
|
||||||
@@ -140,6 +151,72 @@ def normalize_text(text: str) -> str:
|
|||||||
# Приводим к нижнему регистру
|
# Приводим к нижнему регистру
|
||||||
normalized = text.lower()
|
normalized = text.lower()
|
||||||
|
|
||||||
|
# 1. Транслитерация латиницы → кириллица (защита от обхода через латиницу)
|
||||||
|
# Основано на визуальном сходстве букв
|
||||||
|
transliteration_map = {
|
||||||
|
'a': 'а', # a → а
|
||||||
|
'b': 'в', # b → в
|
||||||
|
'c': 'с', # c → с (латинская c похожа на кириллическую с)
|
||||||
|
'd': 'д', # d → д
|
||||||
|
'e': 'е', # e → е
|
||||||
|
'f': 'ф', # f → ф
|
||||||
|
'g': 'г', # g → г
|
||||||
|
'h': 'н', # h → н
|
||||||
|
'i': 'и', # i → и
|
||||||
|
'j': 'ж', # j → ж
|
||||||
|
'k': 'к', # k → к
|
||||||
|
'l': 'л', # l → л
|
||||||
|
'm': 'м', # m → м
|
||||||
|
'n': 'н', # n → н
|
||||||
|
'o': 'о', # o → о
|
||||||
|
'p': 'р', # p → р
|
||||||
|
'q': 'к', # q → к
|
||||||
|
'r': 'р', # r → р
|
||||||
|
's': 'с', # s → с
|
||||||
|
't': 'т', # t → т
|
||||||
|
'u': 'и', # u → и
|
||||||
|
'v': 'в', # v → в
|
||||||
|
'w': 'в', # w → в
|
||||||
|
'x': 'х', # x → х
|
||||||
|
'y': 'у', # y → у
|
||||||
|
'z': 'з', # z → з
|
||||||
|
}
|
||||||
|
|
||||||
|
# Применяем транслитерацию
|
||||||
|
for lat, cyr in transliteration_map.items():
|
||||||
|
normalized = normalized.replace(lat, cyr)
|
||||||
|
|
||||||
|
# 2. Замена цифр на похожие буквы
|
||||||
|
digit_map = {
|
||||||
|
'0': 'о', # 0 → о
|
||||||
|
'1': 'и', # 1 → и (или l)
|
||||||
|
'3': 'з', # 3 → з
|
||||||
|
'4': 'ч', # 4 → ч
|
||||||
|
'6': 'б', # 6 → б
|
||||||
|
'8': 'в', # 8 → в
|
||||||
|
}
|
||||||
|
|
||||||
|
for digit, letter in digit_map.items():
|
||||||
|
normalized = normalized.replace(digit, letter)
|
||||||
|
|
||||||
|
# 3. Языковые паттерны (унификация написания)
|
||||||
|
normalized = normalized.replace('ё', 'е') # ё → е
|
||||||
|
|
||||||
|
# 4. ASCII-имитации букв (сложные комбинации символов)
|
||||||
|
# Важно: делать это ДО удаления других символов
|
||||||
|
ascii_patterns = [
|
||||||
|
(r'\|\|', 'и'), # |\| → и (вертикальные черты)
|
||||||
|
(r'/\\', 'л'), # /\ → л (треугольник)
|
||||||
|
(r'><', 'х'), # >< → х
|
||||||
|
(r'\}\{', 'х'), # }{ → х
|
||||||
|
(r'\)\(', 'х'), # )( → х
|
||||||
|
(r'>\|<', 'ж'), # >|< → ж
|
||||||
|
(r'\}\|\{', 'ж'), # }|{ → ж
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, replacement in ascii_patterns:
|
||||||
|
normalized = re.sub(pattern, replacement, normalized)
|
||||||
|
|
||||||
# Циклически убираем обфускацию, пока что-то меняется
|
# Циклически убираем обфускацию, пока что-то меняется
|
||||||
max_iterations = 10
|
max_iterations = 10
|
||||||
for _ in range(max_iterations):
|
for _ in range(max_iterations):
|
||||||
@@ -170,11 +247,12 @@ def contains_bad_word(text: str) -> bool:
|
|||||||
"""
|
"""
|
||||||
Проверяет, содержит ли текст бранные слова.
|
Проверяет, содержит ли текст бранные слова.
|
||||||
|
|
||||||
Использует:
|
Использует комбинированный подход для минимизации ложных срабатываний:
|
||||||
- Нормализацию текста для обхода обфускации
|
- Нормализацию текста для обхода обфускации
|
||||||
- Проверку границ слов для whole_word паттернов
|
- Проверку границ слов для whole_word паттернов
|
||||||
- Проверку подстрок для contains паттернов
|
- Проверку начала слова для word_start паттернов (НОВОЕ!)
|
||||||
- Список исключений
|
- Проверку подстрок для contains паттернов с минимальной длиной
|
||||||
|
- Расширенный список исключений
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Текст для проверки
|
text: Текст для проверки
|
||||||
@@ -189,33 +267,72 @@ def contains_bad_word(text: str) -> bool:
|
|||||||
normalized_text = normalize_text(text)
|
normalized_text = normalize_text(text)
|
||||||
|
|
||||||
# Получаем паттерны и исключения
|
# Получаем паттерны и исключения
|
||||||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
global _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||||||
|
|
||||||
# Если кэш не загружен, загружаем
|
# Если кэш не загружен, загружаем
|
||||||
if _whole_word_patterns_cache is None:
|
if _whole_word_patterns_cache is None:
|
||||||
load_bad_words()
|
load_bad_words()
|
||||||
|
|
||||||
whole_word_patterns = _whole_word_patterns_cache or []
|
whole_word_patterns = _whole_word_patterns_cache or []
|
||||||
|
word_start_patterns = _word_start_patterns_cache or []
|
||||||
contains_patterns = _contains_patterns_cache or []
|
contains_patterns = _contains_patterns_cache or []
|
||||||
exceptions = _exceptions_cache or []
|
exceptions = _exceptions_cache or []
|
||||||
|
|
||||||
# 1. Проверяем whole_word паттерны (только целые слова)
|
# Вспомогательная функция для проверки исключений
|
||||||
|
def is_in_exception(pattern_pos, pattern_len, text, exceptions_list):
|
||||||
|
"""Проверяет, входит ли найденный паттерн в слово-исключение"""
|
||||||
|
for exception in exceptions_list:
|
||||||
|
# Нормализуем исключение
|
||||||
|
norm_exception = normalize_text(exception)
|
||||||
|
if pattern_len == 0 or len(norm_exception) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ищем исключение в тексте около найденного паттерна
|
||||||
|
# Проверяем область от (pos - len(exception)) до (pos + len(pattern))
|
||||||
|
search_start = max(0, pattern_pos - len(norm_exception))
|
||||||
|
search_end = min(len(text), pattern_pos + pattern_len + len(norm_exception))
|
||||||
|
search_area = text[search_start:search_end]
|
||||||
|
|
||||||
|
if norm_exception in search_area:
|
||||||
|
exc_pos = search_area.find(norm_exception)
|
||||||
|
abs_exc_pos = search_start + exc_pos
|
||||||
|
abs_exc_end = abs_exc_pos + len(norm_exception)
|
||||||
|
|
||||||
|
# Проверяем, что паттерн полностью внутри исключения
|
||||||
|
if abs_exc_pos <= pattern_pos < abs_exc_end:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 1. Проверяем whole_word паттерны (только целые слова с границами \b)
|
||||||
for pattern in whole_word_patterns:
|
for pattern in whole_word_patterns:
|
||||||
# Используем границы слов \b для поиска только целых слов
|
# Используем границы слов \b для поиска только целых слов
|
||||||
regex = r'\b' + re.escape(pattern) + r'\b'
|
regex = r'\b' + re.escape(pattern) + r'\b'
|
||||||
if re.search(regex, normalized_text, re.IGNORECASE):
|
match = re.search(regex, normalized_text, re.IGNORECASE)
|
||||||
# Проверяем, не входит ли в исключения
|
if match:
|
||||||
is_exception = False
|
if not is_in_exception(match.start(), len(pattern), normalized_text, exceptions):
|
||||||
for exception in exceptions:
|
|
||||||
if exception in normalized_text and pattern in exception:
|
|
||||||
is_exception = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not is_exception:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 2. Проверяем contains паттерны (любое вхождение)
|
# 2. Проверяем word_start паттерны (НОВОЕ! только начало слова)
|
||||||
|
# Паттерн должен быть либо в начале строки, либо после не-буквы
|
||||||
|
for pattern in word_start_patterns:
|
||||||
|
# Regex: начало строки ИЛИ не-буква, затем паттерн
|
||||||
|
# (?:^|(?<=[^а-яёa-z])) - положительный lookbehind для начала или не-буквы
|
||||||
|
regex = r'(?:^|(?<=[^а-яёa-z]))' + re.escape(pattern)
|
||||||
|
match = re.search(regex, normalized_text, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
if not is_in_exception(match.start(), len(pattern), normalized_text, exceptions):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 3. Проверяем contains паттерны (любое вхождение)
|
||||||
|
# Применяем минимальную длину для снижения false positives
|
||||||
|
MIN_CONTAINS_LENGTH = 4
|
||||||
|
|
||||||
for pattern in contains_patterns:
|
for pattern in contains_patterns:
|
||||||
|
# Пропускаем слишком короткие паттерны (для безопасности)
|
||||||
|
if len(pattern) < MIN_CONTAINS_LENGTH:
|
||||||
|
logger.warning(f"Паттерн '{pattern}' слишком короткий для contains, пропускаем")
|
||||||
|
continue
|
||||||
|
|
||||||
if pattern in normalized_text:
|
if pattern in normalized_text:
|
||||||
# Проверяем все вхождения паттерна
|
# Проверяем все вхождения паттерна
|
||||||
start = 0
|
start = 0
|
||||||
@@ -224,18 +341,7 @@ def contains_bad_word(text: str) -> bool:
|
|||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Проверяем, не входит ли в исключение
|
if not is_in_exception(pos, len(pattern), normalized_text, exceptions):
|
||||||
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
|
return True
|
||||||
|
|
||||||
start = pos + 1
|
start = pos + 1
|
||||||
@@ -246,11 +352,12 @@ def get_bad_words_from_text(text: str) -> list:
|
|||||||
"""
|
"""
|
||||||
Возвращает список найденных бранных слов в тексте.
|
Возвращает список найденных бранных слов в тексте.
|
||||||
|
|
||||||
Использует:
|
Использует комбинированный подход для минимизации ложных срабатываний:
|
||||||
- Нормализацию текста для обхода обфускации
|
- Нормализацию текста для обхода обфускации
|
||||||
- Проверку границ слов для whole_word паттернов
|
- Проверку границ слов для whole_word паттернов
|
||||||
- Проверку подстрок для contains паттернов
|
- Проверку начала слова для word_start паттернов (НОВОЕ!)
|
||||||
- Список исключений
|
- Проверку подстрок для contains паттернов с минимальной длиной
|
||||||
|
- Расширенный список исключений
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Текст для проверки
|
text: Текст для проверки
|
||||||
@@ -266,35 +373,62 @@ def get_bad_words_from_text(text: str) -> list:
|
|||||||
found_words = []
|
found_words = []
|
||||||
|
|
||||||
# Получаем паттерны и исключения
|
# Получаем паттерны и исключения
|
||||||
global _whole_word_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
global _whole_word_patterns_cache, _word_start_patterns_cache, _contains_patterns_cache, _exceptions_cache
|
||||||
|
|
||||||
# Если кэш не загружен, загружаем
|
# Если кэш не загружен, загружаем
|
||||||
if _whole_word_patterns_cache is None:
|
if _whole_word_patterns_cache is None:
|
||||||
load_bad_words()
|
load_bad_words()
|
||||||
|
|
||||||
whole_word_patterns = _whole_word_patterns_cache or []
|
whole_word_patterns = _whole_word_patterns_cache or []
|
||||||
|
word_start_patterns = _word_start_patterns_cache or []
|
||||||
contains_patterns = _contains_patterns_cache or []
|
contains_patterns = _contains_patterns_cache or []
|
||||||
exceptions = _exceptions_cache or []
|
exceptions = _exceptions_cache or []
|
||||||
|
|
||||||
|
# Вспомогательная функция для проверки исключений
|
||||||
|
def is_in_exception(pattern_pos, pattern_len, text, exceptions_list):
|
||||||
|
"""Проверяет, входит ли найденный паттерн в слово-исключение"""
|
||||||
|
for exception in exceptions_list:
|
||||||
|
norm_exception = normalize_text(exception)
|
||||||
|
if pattern_len == 0 or len(norm_exception) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
search_start = max(0, pattern_pos - len(norm_exception))
|
||||||
|
search_end = min(len(text), pattern_pos + pattern_len + len(norm_exception))
|
||||||
|
search_area = text[search_start:search_end]
|
||||||
|
|
||||||
|
if norm_exception in search_area:
|
||||||
|
exc_pos = search_area.find(norm_exception)
|
||||||
|
abs_exc_pos = search_start + exc_pos
|
||||||
|
abs_exc_end = abs_exc_pos + len(norm_exception)
|
||||||
|
|
||||||
|
if abs_exc_pos <= pattern_pos < abs_exc_end:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
# 1. Проверяем whole_word паттерны (только целые слова)
|
# 1. Проверяем whole_word паттерны (только целые слова)
|
||||||
for pattern in whole_word_patterns:
|
for pattern in whole_word_patterns:
|
||||||
# Используем границы слов \b для поиска только целых слов
|
|
||||||
regex = r'\b' + re.escape(pattern) + r'\b'
|
regex = r'\b' + re.escape(pattern) + r'\b'
|
||||||
if re.search(regex, normalized_text, re.IGNORECASE):
|
match = re.search(regex, normalized_text, re.IGNORECASE)
|
||||||
# Проверяем, не входит ли в исключения
|
if match:
|
||||||
is_exception = False
|
if not is_in_exception(match.start(), len(pattern), normalized_text, exceptions) and pattern not in found_words:
|
||||||
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)
|
found_words.append(pattern)
|
||||||
|
|
||||||
# 2. Проверяем contains паттерны (любое вхождение)
|
# 2. Проверяем word_start паттерны (НОВОЕ! только начало слова)
|
||||||
|
for pattern in word_start_patterns:
|
||||||
|
regex = r'(?:^|(?<=[^а-яёa-z]))' + re.escape(pattern)
|
||||||
|
match = re.search(regex, normalized_text, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
if not is_in_exception(match.start(), len(pattern), normalized_text, exceptions) and pattern not in found_words:
|
||||||
|
found_words.append(pattern)
|
||||||
|
|
||||||
|
# 3. Проверяем contains паттерны (любое вхождение)
|
||||||
|
MIN_CONTAINS_LENGTH = 4
|
||||||
|
|
||||||
for pattern in contains_patterns:
|
for pattern in contains_patterns:
|
||||||
|
if len(pattern) < MIN_CONTAINS_LENGTH:
|
||||||
|
continue
|
||||||
|
|
||||||
if pattern in normalized_text:
|
if pattern in normalized_text:
|
||||||
# Проверяем все вхождения паттерна
|
|
||||||
start = 0
|
start = 0
|
||||||
word_is_valid = False
|
word_is_valid = False
|
||||||
while True:
|
while True:
|
||||||
@@ -302,24 +436,12 @@ def get_bad_words_from_text(text: str) -> list:
|
|||||||
if pos == -1:
|
if pos == -1:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Проверяем, не входит ли в исключение
|
if not is_in_exception(pos, len(pattern), normalized_text, exceptions):
|
||||||
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
|
word_is_valid = True
|
||||||
break
|
break
|
||||||
|
|
||||||
start = pos + 1
|
start = pos + 1
|
||||||
|
|
||||||
# Добавляем слово только если оно действительно найдено (не в исключении)
|
|
||||||
if word_is_valid and pattern not in found_words:
|
if word_is_valid and pattern not in found_words:
|
||||||
found_words.append(pattern)
|
found_words.append(pattern)
|
||||||
|
|
||||||
|
|||||||
@@ -19,24 +19,29 @@
|
|||||||
"гнида",
|
"гнида",
|
||||||
"гнид"
|
"гнид"
|
||||||
],
|
],
|
||||||
"contains": [
|
"word_start": [
|
||||||
"хуй",
|
"хуй",
|
||||||
"хуе",
|
"хуе",
|
||||||
"хуи",
|
"хуи",
|
||||||
"хую",
|
"хую",
|
||||||
"хуя",
|
"хуя",
|
||||||
"хер",
|
"хер",
|
||||||
|
"бля",
|
||||||
"пизд",
|
"пизд",
|
||||||
"пизж",
|
"пизж",
|
||||||
"пезд",
|
"пезд",
|
||||||
"ебал",
|
"ебал",
|
||||||
"ебан",
|
"ебан",
|
||||||
"ебат",
|
"ебат",
|
||||||
"ебу",
|
|
||||||
"ебош",
|
"ебош",
|
||||||
"ебля",
|
"ебля",
|
||||||
"ебет",
|
"ебет",
|
||||||
"бля",
|
"ебут",
|
||||||
|
"ебуч",
|
||||||
|
"жоп",
|
||||||
|
"жёп"
|
||||||
|
],
|
||||||
|
"contains": [
|
||||||
"блядь",
|
"блядь",
|
||||||
"блять",
|
"блять",
|
||||||
"сука",
|
"сука",
|
||||||
@@ -64,8 +69,6 @@
|
|||||||
"серун",
|
"серун",
|
||||||
"дрочи",
|
"дрочи",
|
||||||
"дроч",
|
"дроч",
|
||||||
"жоп",
|
|
||||||
"жёп",
|
|
||||||
"залуп",
|
"залуп",
|
||||||
"ублюдо",
|
"ублюдо",
|
||||||
"ублюд",
|
"ублюд",
|
||||||
@@ -90,7 +93,25 @@
|
|||||||
"exceptions": [
|
"exceptions": [
|
||||||
"республика",
|
"республика",
|
||||||
"документ",
|
"документ",
|
||||||
"документы"
|
"документы",
|
||||||
|
"требует",
|
||||||
|
"требуется",
|
||||||
|
"требую",
|
||||||
|
"требуем",
|
||||||
|
"требуют",
|
||||||
|
"требовать",
|
||||||
|
"требование",
|
||||||
|
"потребует",
|
||||||
|
"потребуется",
|
||||||
|
"употреблять",
|
||||||
|
"употребляю",
|
||||||
|
"употребление",
|
||||||
|
"потребление",
|
||||||
|
"требуха",
|
||||||
|
"скребу",
|
||||||
|
"скребут",
|
||||||
|
"гребу",
|
||||||
|
"гребут"
|
||||||
],
|
],
|
||||||
"_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки"
|
"_comment": "whole_word - только целые слова с границами \\b, word_start - только начало слова, contains - любое вхождение (мин. 4 буквы в коде)"
|
||||||
}
|
}
|
||||||
103
src/database.py
103
src/database.py
@@ -110,6 +110,15 @@ class Database: # Инициализация класса
|
|||||||
ON message_cache(timestamp)
|
ON message_cache(timestamp)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# Таблица для отключенных топиков (где не начисляется карма)
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS disabled_karma_topics (
|
||||||
|
chat_id INTEGER NOT NULL,
|
||||||
|
thread_id INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (chat_id, thread_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
connect.commit()
|
connect.commit()
|
||||||
logger.info("База данных и индексы успешно инициализированы")
|
logger.info("База данных и индексы успешно инициализированы")
|
||||||
|
|
||||||
@@ -460,5 +469,99 @@ class Database: # Инициализация класса
|
|||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
return result[0] if result else 0
|
return result[0] if result else 0
|
||||||
|
|
||||||
|
# Проверяет, отключена ли карма в топике
|
||||||
|
def is_karma_disabled_in_topic(self, chat_id: int, thread_id: Optional[int]) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, отключена ли карма в данном топике.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: ID чата
|
||||||
|
thread_id: ID топика (None для главного чата)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если карма отключена, иначе False
|
||||||
|
"""
|
||||||
|
# Если thread_id None, считаем что это главный чат (карма всегда включена)
|
||||||
|
if thread_id is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
with self._get_connection() as connect:
|
||||||
|
cursor = connect.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT 1 FROM disabled_karma_topics
|
||||||
|
WHERE chat_id = ? AND thread_id = ?
|
||||||
|
''', (chat_id, thread_id))
|
||||||
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
# Отключает карму в топике
|
||||||
|
def disable_karma_in_topic(self, chat_id: int, thread_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Отключает начисление кармы в данном топике.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: ID чата
|
||||||
|
thread_id: ID топика
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если успешно, False если уже было отключено
|
||||||
|
"""
|
||||||
|
with self._get_connection() as connect:
|
||||||
|
cursor = connect.cursor()
|
||||||
|
|
||||||
|
# Проверяем, не отключено ли уже
|
||||||
|
if self.is_karma_disabled_in_topic(chat_id, thread_id):
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO disabled_karma_topics (chat_id, thread_id)
|
||||||
|
VALUES (?, ?)
|
||||||
|
''', (chat_id, thread_id))
|
||||||
|
connect.commit()
|
||||||
|
logger.info(f"Карма отключена в топике {thread_id} чата {chat_id}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Включает карму в топике
|
||||||
|
def enable_karma_in_topic(self, chat_id: int, thread_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Включает начисление кармы в данном топике.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: ID чата
|
||||||
|
thread_id: ID топика
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если успешно, False если уже было включено
|
||||||
|
"""
|
||||||
|
with self._get_connection() as connect:
|
||||||
|
cursor = connect.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
DELETE FROM disabled_karma_topics
|
||||||
|
WHERE chat_id = ? AND thread_id = ?
|
||||||
|
''', (chat_id, thread_id))
|
||||||
|
deleted = cursor.rowcount > 0
|
||||||
|
connect.commit()
|
||||||
|
if deleted:
|
||||||
|
logger.info(f"Карма включена в топике {thread_id} чата {chat_id}")
|
||||||
|
return deleted
|
||||||
|
|
||||||
|
# Получает список отключенных топиков для чата
|
||||||
|
def get_disabled_topics(self, chat_id: int) -> list:
|
||||||
|
"""
|
||||||
|
Возвращает список ID топиков, где отключена карма.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
chat_id: ID чата
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список ID топиков
|
||||||
|
"""
|
||||||
|
with self._get_connection() as connect:
|
||||||
|
cursor = connect.cursor()
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT thread_id FROM disabled_karma_topics
|
||||||
|
WHERE chat_id = ?
|
||||||
|
''', (chat_id,))
|
||||||
|
return [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
# Создаем экземпляр базы данных для импорта в других модулях
|
# Создаем экземпляр базы данных для импорта в других модулях
|
||||||
db = Database()
|
db = Database()
|
||||||
@@ -53,6 +53,7 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
- Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы
|
- Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы
|
||||||
- Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы
|
- Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы
|
||||||
- Поставил ❤🔥 → +10 кармы | Убрал ❤🔥 → -10 кармы
|
- Поставил ❤🔥 → +10 кармы | Убрал ❤🔥 → -10 кармы
|
||||||
|
- Поставил 🤡 → -20 кармы ПОСТАВИВШЕМУ (не возвращается при снятии!)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"[KARMA] Получена реакция от {reaction.user.id}")
|
logger.info(f"[KARMA] Получена реакция от {reaction.user.id}")
|
||||||
@@ -73,6 +74,11 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
|
|
||||||
to_user_id, message_thread_id = cached_data
|
to_user_id, message_thread_id = cached_data
|
||||||
|
|
||||||
|
# Проверяем, отключена ли карма в этом топике
|
||||||
|
if db.is_karma_disabled_in_topic(chat_id, message_thread_id):
|
||||||
|
logger.info(f"[KARMA] Пропуск реакции - карма отключена в топике {message_thread_id}")
|
||||||
|
return
|
||||||
|
|
||||||
# Защита от самооценки
|
# Защита от самооценки
|
||||||
if from_user.id == to_user_id:
|
if from_user.id == to_user_id:
|
||||||
logger.info(f"Пользователь {from_user.id} попытался поставить реакцию на своё сообщение")
|
logger.info(f"Пользователь {from_user.id} попытался поставить реакцию на своё сообщение")
|
||||||
@@ -94,6 +100,7 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
old_heart = False
|
old_heart = False
|
||||||
old_fire_heart = False
|
old_fire_heart = False
|
||||||
old_fire = False
|
old_fire = False
|
||||||
|
old_clown = False
|
||||||
if reaction.old_reaction:
|
if reaction.old_reaction:
|
||||||
for react in reaction.old_reaction:
|
for react in reaction.old_reaction:
|
||||||
if isinstance(react, ReactionTypeEmoji):
|
if isinstance(react, ReactionTypeEmoji):
|
||||||
@@ -107,6 +114,8 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
old_fire_heart = True
|
old_fire_heart = True
|
||||||
elif react.emoji == "🔥":
|
elif react.emoji == "🔥":
|
||||||
old_fire = True
|
old_fire = True
|
||||||
|
elif react.emoji == "🤡":
|
||||||
|
old_clown = True
|
||||||
|
|
||||||
# Проверяем новые реакции
|
# Проверяем новые реакции
|
||||||
new_thumbs_up = False
|
new_thumbs_up = False
|
||||||
@@ -114,6 +123,7 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
new_heart = False
|
new_heart = False
|
||||||
new_fire_heart = False
|
new_fire_heart = False
|
||||||
new_fire = False
|
new_fire = False
|
||||||
|
new_clown = False
|
||||||
if reaction.new_reaction:
|
if reaction.new_reaction:
|
||||||
for react in reaction.new_reaction:
|
for react in reaction.new_reaction:
|
||||||
if isinstance(react, ReactionTypeEmoji):
|
if isinstance(react, ReactionTypeEmoji):
|
||||||
@@ -127,6 +137,8 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
new_fire_heart = True
|
new_fire_heart = True
|
||||||
elif react.emoji == "🔥":
|
elif react.emoji == "🔥":
|
||||||
new_fire = True
|
new_fire = True
|
||||||
|
elif react.emoji == "🤡":
|
||||||
|
new_clown = True
|
||||||
|
|
||||||
# Определяем изменение кармы (накапливаем все изменения)
|
# Определяем изменение кармы (накапливаем все изменения)
|
||||||
karma_change = 0
|
karma_change = 0
|
||||||
@@ -175,19 +187,63 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
karma_change -= 2
|
karma_change -= 2
|
||||||
actions.append("убрал 🔥 (-2)")
|
actions.append("убрал 🔥 (-2)")
|
||||||
|
|
||||||
|
# Проверяем 🤡 (ОСОБАЯ ЛОГИКА: минусует карму поставившему!)
|
||||||
|
clown_penalty = 0
|
||||||
|
if new_clown and not old_clown:
|
||||||
|
# Поставил клоуна → минус 20 кармы ПОСТАВИВШЕМУ
|
||||||
|
clown_penalty = -20
|
||||||
|
logger.info(f"[KARMA] Пользователь {from_user.id} поставил клоуна → -20 кармы ему самому")
|
||||||
|
# При снятии клоуна ничего не делаем (карма не возвращается)
|
||||||
|
|
||||||
# Если нет изменений - выходим
|
# Если нет изменений - выходим
|
||||||
if karma_change == 0:
|
if karma_change == 0 and clown_penalty == 0:
|
||||||
logger.info(f"[KARMA] Нет изменений в реакциях")
|
logger.info(f"[KARMA] Нет изменений в реакциях")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Формируем текст действий для логирования
|
# Формируем текст действий для логирования
|
||||||
action_text = ", ".join(actions)
|
action_text = ", ".join(actions) if actions else ""
|
||||||
logger.info(f"[KARMA] {action_text} от {from_user.id} для {to_user_id}, итоговое изменение кармы: {karma_change}")
|
logger.info(f"[KARMA] {action_text} от {from_user.id} для {to_user_id}, итоговое изменение кармы: {karma_change}")
|
||||||
|
|
||||||
# Изменяем карму
|
# Изменяем карму получателю (если есть изменения)
|
||||||
db.add_karma(to_user_id, chat_id, karma_change)
|
if karma_change != 0:
|
||||||
|
db.add_karma(to_user_id, chat_id, karma_change)
|
||||||
|
|
||||||
# Получаем новую карму
|
# Применяем штраф за клоуна к ПОСТАВИВШЕМУ
|
||||||
|
if clown_penalty != 0:
|
||||||
|
db.add_karma(from_user.id, chat_id, clown_penalty)
|
||||||
|
clown_karma = db.get_karma(from_user.id, chat_id)
|
||||||
|
|
||||||
|
# Формируем имя поставившего
|
||||||
|
from_user_name = from_user.first_name
|
||||||
|
from_user_display = f"@{from_user.username}" if from_user.username else from_user_name
|
||||||
|
|
||||||
|
# Отправляем уведомление о штрафе за клоуна
|
||||||
|
clown_message = f"🤡 {from_user_display} поставил клоуна и получил -20 кармы! Текущая карма: {clown_karma}"
|
||||||
|
try:
|
||||||
|
sent_clown_msg = await bot.send_message(
|
||||||
|
chat_id,
|
||||||
|
clown_message,
|
||||||
|
message_thread_id=message_thread_id
|
||||||
|
)
|
||||||
|
logger.info(f"[KARMA] Уведомление о клоуне отправлено")
|
||||||
|
|
||||||
|
# Удаляем уведомление через 15 секунд
|
||||||
|
async def delete_clown_notification():
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(15)
|
||||||
|
await bot.delete_message(chat_id, sent_clown_msg.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Не удалось удалить уведомление о клоуне: {e}")
|
||||||
|
|
||||||
|
asyncio.create_task(delete_clown_notification())
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка отправки уведомления о клоуне: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# Если нет обычных изменений кармы - выходим (клоун уже обработан)
|
||||||
|
if karma_change == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Получаем новую карму получателя
|
||||||
new_karma = db.get_karma(to_user_id, chat_id)
|
new_karma = db.get_karma(to_user_id, chat_id)
|
||||||
|
|
||||||
# Формируем имя пользователя (из БД: id, nickname, tag)
|
# Формируем имя пользователя (из БД: id, nickname, tag)
|
||||||
@@ -251,6 +307,11 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
logger.info(f"[KARMA] Пропуск - не групповой чат: {message.chat.type}")
|
logger.info(f"[KARMA] Пропуск - не групповой чат: {message.chat.type}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Проверяем, отключена ли карма в этом топике
|
||||||
|
if db.is_karma_disabled_in_topic(message.chat.id, message.message_thread_id):
|
||||||
|
logger.info(f"[KARMA] Пропуск благодарности - карма отключена в топике {message.message_thread_id}")
|
||||||
|
return
|
||||||
|
|
||||||
# ВАЖНО: В топиках каждое сообщение технически является reply на первое сообщение топика
|
# ВАЖНО: В топиках каждое сообщение технически является reply на первое сообщение топика
|
||||||
# Проверяем, что это реальный reply на сообщение пользователя, а не просто сообщение в топике
|
# Проверяем, что это реальный reply на сообщение пользователя, а не просто сообщение в топике
|
||||||
if message.is_topic_message and message.reply_to_message.message_id == message.message_thread_id:
|
if message.is_topic_message and message.reply_to_message.message_id == message.message_thread_id:
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from bad_words import (
|
|||||||
save_bad_words,
|
save_bad_words,
|
||||||
reload_words
|
reload_words
|
||||||
)
|
)
|
||||||
|
from database import db
|
||||||
from utils import check_admin_status, delete_messages
|
from utils import check_admin_status, delete_messages
|
||||||
from config import COMMAND_MESSAGES
|
from config import COMMAND_MESSAGES
|
||||||
|
from action_reporter import action_reporter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -139,6 +141,16 @@ async def add_bad_word(bot: AsyncTeleBot, message: Message, word: str):
|
|||||||
reload_words() # Перезагружаем кэш
|
reload_words() # Перезагружаем кэш
|
||||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в список бранных.")
|
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в список бранных.")
|
||||||
logger.info(f"Администратор {message.from_user.id} добавил бранное слово: {word}")
|
logger.info(f"Администратор {message.from_user.id} добавил бранное слово: {word}")
|
||||||
|
|
||||||
|
# Отправляем в канал аудита
|
||||||
|
if action_reporter:
|
||||||
|
await action_reporter.log_action(
|
||||||
|
action=f"Добавление бранного слова",
|
||||||
|
user_id=None, # Не применимо к конкретному пользователю
|
||||||
|
admin_id=message.from_user.id,
|
||||||
|
reason=f"Слово: <code>{word}</code>",
|
||||||
|
duration=None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||||
|
|
||||||
@@ -158,6 +170,16 @@ async def remove_bad_word(bot: AsyncTeleBot, message: Message, word: str):
|
|||||||
reload_words() # Перезагружаем кэш
|
reload_words() # Перезагружаем кэш
|
||||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из списка бранных.")
|
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из списка бранных.")
|
||||||
logger.info(f"Администратор {message.from_user.id} удалил бранное слово: {word}")
|
logger.info(f"Администратор {message.from_user.id} удалил бранное слово: {word}")
|
||||||
|
|
||||||
|
# Отправляем в канал аудита
|
||||||
|
if action_reporter:
|
||||||
|
await action_reporter.log_action(
|
||||||
|
action=f"Удаление бранного слова",
|
||||||
|
user_id=None,
|
||||||
|
admin_id=message.from_user.id,
|
||||||
|
reason=f"Слово: <code>{word}</code>",
|
||||||
|
duration=None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||||
|
|
||||||
@@ -194,6 +216,16 @@ async def add_exception(bot: AsyncTeleBot, message: Message, word: str):
|
|||||||
reload_words() # Перезагружаем кэш
|
reload_words() # Перезагружаем кэш
|
||||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в исключения.")
|
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' добавлено в исключения.")
|
||||||
logger.info(f"Администратор {message.from_user.id} добавил исключение: {word}")
|
logger.info(f"Администратор {message.from_user.id} добавил исключение: {word}")
|
||||||
|
|
||||||
|
# Отправляем в канал аудита
|
||||||
|
if action_reporter:
|
||||||
|
await action_reporter.log_action(
|
||||||
|
action=f"Добавление исключения",
|
||||||
|
user_id=None,
|
||||||
|
admin_id=message.from_user.id,
|
||||||
|
reason=f"Слово: <code>{word}</code>",
|
||||||
|
duration=None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||||
|
|
||||||
@@ -213,6 +245,16 @@ async def remove_exception(bot: AsyncTeleBot, message: Message, word: str):
|
|||||||
reload_words() # Перезагружаем кэш
|
reload_words() # Перезагружаем кэш
|
||||||
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из исключений.")
|
await send_temp_message(bot, message, f"✅ Слово '<code>{word}</code>' удалено из исключений.")
|
||||||
logger.info(f"Администратор {message.from_user.id} удалил исключение: {word}")
|
logger.info(f"Администратор {message.from_user.id} удалил исключение: {word}")
|
||||||
|
|
||||||
|
# Отправляем в канал аудита
|
||||||
|
if action_reporter:
|
||||||
|
await action_reporter.log_action(
|
||||||
|
action=f"Удаление исключения",
|
||||||
|
user_id=None,
|
||||||
|
admin_id=message.from_user.id,
|
||||||
|
reason=f"Слово: <code>{word}</code>",
|
||||||
|
duration=None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from database import db
|
from database import db
|
||||||
|
from utils import check_admin_status, delete_messages
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -21,11 +22,14 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
@bot.message_handler(commands=['karma', 'rating'])
|
@bot.message_handler(commands=['karma', 'rating'])
|
||||||
async def handle_karma_command(message: Message):
|
async def handle_karma_command(message: Message):
|
||||||
"""
|
"""
|
||||||
Команда /karma - показывает карму пользователя
|
Команда /karma - показывает карму пользователя или управляет настройками кармы
|
||||||
Использование:
|
Использование:
|
||||||
/karma - показать свою карму
|
/karma - показать свою карму
|
||||||
/karma @username - показать карму пользователя
|
/karma @username - показать карму пользователя
|
||||||
/karma (в ответ на сообщение) - показать карму автора сообщения
|
/karma (в ответ на сообщение) - показать карму автора сообщения
|
||||||
|
/karma disable - отключить карму в топике (только админы)
|
||||||
|
/karma enable - включить карму в топике (только админы)
|
||||||
|
/karma status - показать статус кармы в топиках (только админы)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Проверяем, что это групповой чат
|
# Проверяем, что это групповой чат
|
||||||
@@ -34,6 +38,31 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
return
|
return
|
||||||
|
|
||||||
chat_id = message.chat.id
|
chat_id = message.chat.id
|
||||||
|
parts = message.text.split(maxsplit=1)
|
||||||
|
|
||||||
|
# Проверяем субкоманды для админов
|
||||||
|
if len(parts) > 1:
|
||||||
|
subcommand = parts[1].lower()
|
||||||
|
|
||||||
|
# Субкоманды управления кармой (только для админов)
|
||||||
|
if subcommand in ['disable', 'enable', 'status']:
|
||||||
|
# Проверяем права администратора
|
||||||
|
admin_check = await check_admin_status(bot, message, check_restrict_rights=False)
|
||||||
|
if admin_check == 1:
|
||||||
|
logger.info(f"Пользователь {message.from_user.id} не является администратором")
|
||||||
|
return
|
||||||
|
|
||||||
|
if subcommand == 'disable':
|
||||||
|
await disable_karma_in_topic(bot, message)
|
||||||
|
return
|
||||||
|
elif subcommand == 'enable':
|
||||||
|
await enable_karma_in_topic(bot, message)
|
||||||
|
return
|
||||||
|
elif subcommand == 'status':
|
||||||
|
await show_karma_status(bot, message)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Обычная логика показа кармы
|
||||||
target_user = None
|
target_user = None
|
||||||
target_user_id = None
|
target_user_id = None
|
||||||
|
|
||||||
@@ -43,8 +72,8 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
target_user_id = target_user.id
|
target_user_id = target_user.id
|
||||||
|
|
||||||
# Если указан username в команде
|
# Если указан username в команде
|
||||||
elif len(message.text.split()) > 1:
|
elif len(parts) > 1:
|
||||||
username_arg = message.text.split()[1]
|
username_arg = parts[1]
|
||||||
# Убираем @ если есть
|
# Убираем @ если есть
|
||||||
username = username_arg.lstrip('@')
|
username = username_arg.lstrip('@')
|
||||||
|
|
||||||
@@ -136,9 +165,9 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
else:
|
else:
|
||||||
medal = f"{idx}."
|
medal = f"{idx}."
|
||||||
|
|
||||||
# Формируем отображение пользователя
|
# Формируем отображение пользователя (без упоминаний)
|
||||||
if tag:
|
if tag:
|
||||||
user_display = f"@{tag}"
|
user_display = f"<code>@{tag}</code>"
|
||||||
else:
|
else:
|
||||||
user_display = nickname or f"ID: {user_id}"
|
user_display = nickname or f"ID: {user_id}"
|
||||||
|
|
||||||
@@ -152,4 +181,69 @@ def register_handlers(bot: AsyncTeleBot):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при обработке команды /top: {e}", exc_info=True)
|
logger.error(f"Ошибка при обработке команды /top: {e}", exc_info=True)
|
||||||
await bot.reply_to(message, "❌ Произошла ошибка при получении топа")
|
await bot.reply_to(message, "❌ Произошла ошибка при получении топа")
|
||||||
|
|
||||||
|
async def disable_karma_in_topic(bot: AsyncTeleBot, message: Message):
|
||||||
|
"""Отключает начисление кармы в текущем топике"""
|
||||||
|
# Проверяем, что команда вызвана в топике
|
||||||
|
if not message.is_topic_message:
|
||||||
|
await send_temp_message(bot, message, "❌ Эта команда работает только в топиках!")
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = message.message_thread_id
|
||||||
|
chat_id = message.chat.id
|
||||||
|
|
||||||
|
# Отключаем карму в топике
|
||||||
|
if db.disable_karma_in_topic(chat_id, thread_id):
|
||||||
|
await send_temp_message(
|
||||||
|
bot, message,
|
||||||
|
f"🚫 Начисление кармы отключено в этом топике (ID: {thread_id})"
|
||||||
|
)
|
||||||
|
logger.info(f"Администратор {message.from_user.id} отключил карму в топике {thread_id}")
|
||||||
|
else:
|
||||||
|
await send_temp_message(bot, message, "⚠️ Карма уже отключена в этом топике")
|
||||||
|
|
||||||
|
async def enable_karma_in_topic(bot: AsyncTeleBot, message: Message):
|
||||||
|
"""Включает начисление кармы в текущем топике"""
|
||||||
|
# Проверяем, что команда вызвана в топике
|
||||||
|
if not message.is_topic_message:
|
||||||
|
await send_temp_message(bot, message, "❌ Эта команда работает только в топиках!")
|
||||||
|
return
|
||||||
|
|
||||||
|
thread_id = message.message_thread_id
|
||||||
|
chat_id = message.chat.id
|
||||||
|
|
||||||
|
# Включаем карму в топике
|
||||||
|
if db.enable_karma_in_topic(chat_id, thread_id):
|
||||||
|
await send_temp_message(
|
||||||
|
bot, message,
|
||||||
|
f"✅ Начисление кармы включено в этом топике (ID: {thread_id})"
|
||||||
|
)
|
||||||
|
logger.info(f"Администратор {message.from_user.id} включил карму в топике {thread_id}")
|
||||||
|
else:
|
||||||
|
await send_temp_message(bot, message, "⚠️ Карма уже включена в этом топике")
|
||||||
|
|
||||||
|
async def show_karma_status(bot: AsyncTeleBot, message: Message):
|
||||||
|
"""Показывает статус кармы во всех топиках чата"""
|
||||||
|
chat_id = message.chat.id
|
||||||
|
|
||||||
|
# Получаем список отключенных топиков
|
||||||
|
disabled_topics = db.get_disabled_topics(chat_id)
|
||||||
|
|
||||||
|
if not disabled_topics:
|
||||||
|
text = "✅ Карма включена во всех топиках"
|
||||||
|
else:
|
||||||
|
text = f"📊 <b>Статус кармы в топиках</b>\n\n"
|
||||||
|
text += f"🚫 Отключено в топиках (ID): {', '.join(map(str, disabled_topics))}\n"
|
||||||
|
text += f"\n💡 Используйте <code>/karma enable</code> в топике, чтобы включить карму"
|
||||||
|
|
||||||
|
await send_temp_message(bot, message, text, time_sleep=20)
|
||||||
|
|
||||||
|
async def send_temp_message(bot: AsyncTeleBot, message: Message, text: str, time_sleep: int = 10):
|
||||||
|
"""Отправляет временное сообщение, которое удаляется через указанное время"""
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text=text,
|
||||||
|
message_thread_id=message.message_thread_id,
|
||||||
|
)
|
||||||
|
await delete_messages(bot, message, time_sleep=time_sleep, number_message=2)
|
||||||
Reference in New Issue
Block a user