diff --git a/src/action_reporter.py b/src/action_reporter.py
index eceeeb6..9090248 100644
--- a/src/action_reporter.py
+++ b/src/action_reporter.py
@@ -59,23 +59,27 @@ class ActionReporter:
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:
# Получаем информацию о пользователе и администраторе
- user_info = await self._get_user_info(user_id)
admin_info = await self._get_admin_info(admin_id)
-
+
# Формируем сообщение
msg = f"⚙️ Действие: {action}\n"
-
+
if duration:
msg += f"⏱ Длительность: {duration}\n"
-
+
if reason:
msg += f"📝 Причина: {reason}\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):
diff --git a/src/bad_words.py b/src/bad_words.py
index f1a95be..ea70286 100644
--- a/src/bad_words.py
+++ b/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
_exceptions_cache = None
_whole_word_patterns_cache = None
+_word_start_patterns_cache = None
_contains_patterns_cache = None
def load_bad_words():
@@ -25,7 +26,7 @@ def load_bad_words():
Returns:
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:
with open(BAD_WORDS_FILE, 'r', encoding='utf-8') as f:
@@ -36,14 +37,16 @@ def load_bad_words():
# Новый формат с паттернами
patterns = data['patterns']
_whole_word_patterns_cache = patterns.get('whole_word', [])
+ _word_start_patterns_cache = patterns.get('word_start', [])
_contains_patterns_cache = patterns.get('contains', [])
_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(
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)} исключений"
)
else:
@@ -51,6 +54,7 @@ def load_bad_words():
_bad_words_cache = data.get('bad_words', [])
_exceptions_cache = data.get('exceptions', [])
_whole_word_patterns_cache = []
+ _word_start_patterns_cache = []
_contains_patterns_cache = _bad_words_cache.copy()
logger.info(f"Загружено {len(_bad_words_cache)} бранных слов (старый формат) и {len(_exceptions_cache)} исключений")
@@ -114,9 +118,12 @@ def get_exceptions():
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
_exceptions_cache = None
+ _whole_word_patterns_cache = None
+ _word_start_patterns_cache = None
+ _contains_patterns_cache = None
return load_bad_words()
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()
+ # 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
for _ in range(max_iterations):
@@ -170,11 +247,12 @@ def contains_bad_word(text: str) -> bool:
"""
Проверяет, содержит ли текст бранные слова.
- Использует:
+ Использует комбинированный подход для минимизации ложных срабатываний:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- - Проверку подстрок для contains паттернов
- - Список исключений
+ - Проверку начала слова для word_start паттернов (НОВОЕ!)
+ - Проверку подстрок для contains паттернов с минимальной длиной
+ - Расширенный список исключений
Args:
text: Текст для проверки
@@ -189,33 +267,72 @@ def contains_bad_word(text: str) -> bool:
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:
load_bad_words()
whole_word_patterns = _whole_word_patterns_cache or []
+ word_start_patterns = _word_start_patterns_cache or []
contains_patterns = _contains_patterns_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:
# Используем границы слов \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:
+ match = re.search(regex, normalized_text, re.IGNORECASE)
+ if match:
+ if not is_in_exception(match.start(), len(pattern), normalized_text, exceptions):
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:
+ # Пропускаем слишком короткие паттерны (для безопасности)
+ if len(pattern) < MIN_CONTAINS_LENGTH:
+ logger.warning(f"Паттерн '{pattern}' слишком короткий для contains, пропускаем")
+ continue
+
if pattern in normalized_text:
# Проверяем все вхождения паттерна
start = 0
@@ -224,18 +341,7 @@ def contains_bad_word(text: str) -> bool:
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:
+ if not is_in_exception(pos, len(pattern), normalized_text, exceptions):
return True
start = pos + 1
@@ -246,11 +352,12 @@ def get_bad_words_from_text(text: str) -> list:
"""
Возвращает список найденных бранных слов в тексте.
- Использует:
+ Использует комбинированный подход для минимизации ложных срабатываний:
- Нормализацию текста для обхода обфускации
- Проверку границ слов для whole_word паттернов
- - Проверку подстрок для contains паттернов
- - Список исключений
+ - Проверку начала слова для word_start паттернов (НОВОЕ!)
+ - Проверку подстрок для contains паттернов с минимальной длиной
+ - Расширенный список исключений
Args:
text: Текст для проверки
@@ -266,35 +373,62 @@ def get_bad_words_from_text(text: str) -> list:
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:
load_bad_words()
whole_word_patterns = _whole_word_patterns_cache or []
+ word_start_patterns = _word_start_patterns_cache or []
contains_patterns = _contains_patterns_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 паттерны (только целые слова)
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:
+ 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)
- # 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:
+ if len(pattern) < MIN_CONTAINS_LENGTH:
+ continue
+
if pattern in normalized_text:
- # Проверяем все вхождения паттерна
start = 0
word_is_valid = False
while True:
@@ -302,24 +436,12 @@ def get_bad_words_from_text(text: str) -> list:
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:
+ if not is_in_exception(pos, len(pattern), normalized_text, exceptions):
word_is_valid = True
break
start = pos + 1
- # Добавляем слово только если оно действительно найдено (не в исключении)
if word_is_valid and pattern not in found_words:
found_words.append(pattern)
diff --git a/src/data/bad_words.json b/src/data/bad_words.json
index eecac7f..62ae7fe 100644
--- a/src/data/bad_words.json
+++ b/src/data/bad_words.json
@@ -19,24 +19,29 @@
"гнида",
"гнид"
],
- "contains": [
+ "word_start": [
"хуй",
"хуе",
"хуи",
"хую",
"хуя",
"хер",
+ "бля",
"пизд",
"пизж",
"пезд",
"ебал",
"ебан",
"ебат",
- "ебу",
"ебош",
"ебля",
"ебет",
- "бля",
+ "ебут",
+ "ебуч",
+ "жоп",
+ "жёп"
+ ],
+ "contains": [
"блядь",
"блять",
"сука",
@@ -64,8 +69,6 @@
"серун",
"дрочи",
"дроч",
- "жоп",
- "жёп",
"залуп",
"ублюдо",
"ублюд",
@@ -90,7 +93,25 @@
"exceptions": [
"республика",
"документ",
- "документы"
+ "документы",
+ "требует",
+ "требуется",
+ "требую",
+ "требуем",
+ "требуют",
+ "требовать",
+ "требование",
+ "потребует",
+ "потребуется",
+ "употреблять",
+ "употребляю",
+ "употребление",
+ "потребление",
+ "требуха",
+ "скребу",
+ "скребут",
+ "гребу",
+ "гребут"
],
- "_comment": "whole_word - только целые слова (не часть другого слова), contains - любое вхождение подстроки"
+ "_comment": "whole_word - только целые слова с границами \\b, word_start - только начало слова, contains - любое вхождение (мин. 4 буквы в коде)"
}
\ No newline at end of file
diff --git a/src/database.py b/src/database.py
index 4585c22..bf57221 100644
--- a/src/database.py
+++ b/src/database.py
@@ -110,6 +110,15 @@ class Database: # Инициализация класса
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()
logger.info("База данных и индексы успешно инициализированы")
@@ -460,5 +469,99 @@ class Database: # Инициализация класса
result = cursor.fetchone()
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()
\ No newline at end of file
diff --git a/src/modules/0_karma_tracker.py b/src/modules/0_karma_tracker.py
index aed6ef9..1bc9252 100644
--- a/src/modules/0_karma_tracker.py
+++ b/src/modules/0_karma_tracker.py
@@ -53,6 +53,7 @@ def register_handlers(bot: AsyncTeleBot):
- Поставил 🔥 → +2 кармы | Убрал 🔥 → -2 кармы
- Поставил ❤ → +5 кармы | Убрал ❤ → -5 кармы
- Поставил ❤🔥 → +10 кармы | Убрал ❤🔥 → -10 кармы
+ - Поставил 🤡 → -20 кармы ПОСТАВИВШЕМУ (не возвращается при снятии!)
"""
try:
logger.info(f"[KARMA] Получена реакция от {reaction.user.id}")
@@ -73,6 +74,11 @@ def register_handlers(bot: AsyncTeleBot):
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:
logger.info(f"Пользователь {from_user.id} попытался поставить реакцию на своё сообщение")
@@ -94,6 +100,7 @@ def register_handlers(bot: AsyncTeleBot):
old_heart = False
old_fire_heart = False
old_fire = False
+ old_clown = False
if reaction.old_reaction:
for react in reaction.old_reaction:
if isinstance(react, ReactionTypeEmoji):
@@ -107,6 +114,8 @@ def register_handlers(bot: AsyncTeleBot):
old_fire_heart = True
elif react.emoji == "🔥":
old_fire = True
+ elif react.emoji == "🤡":
+ old_clown = True
# Проверяем новые реакции
new_thumbs_up = False
@@ -114,6 +123,7 @@ def register_handlers(bot: AsyncTeleBot):
new_heart = False
new_fire_heart = False
new_fire = False
+ new_clown = False
if reaction.new_reaction:
for react in reaction.new_reaction:
if isinstance(react, ReactionTypeEmoji):
@@ -127,6 +137,8 @@ def register_handlers(bot: AsyncTeleBot):
new_fire_heart = True
elif react.emoji == "🔥":
new_fire = True
+ elif react.emoji == "🤡":
+ new_clown = True
# Определяем изменение кармы (накапливаем все изменения)
karma_change = 0
@@ -175,19 +187,63 @@ def register_handlers(bot: AsyncTeleBot):
karma_change -= 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] Нет изменений в реакциях")
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}")
- # Изменяем карму
- 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)
# Формируем имя пользователя (из БД: id, nickname, tag)
@@ -251,6 +307,11 @@ def register_handlers(bot: AsyncTeleBot):
logger.info(f"[KARMA] Пропуск - не групповой чат: {message.chat.type}")
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 на сообщение пользователя, а не просто сообщение в топике
if message.is_topic_message and message.reply_to_message.message_id == message.message_thread_id:
diff --git a/src/modules/badwords_manager.py b/src/modules/badwords_manager.py
index 165682f..0186a88 100644
--- a/src/modules/badwords_manager.py
+++ b/src/modules/badwords_manager.py
@@ -8,8 +8,10 @@ from bad_words import (
save_bad_words,
reload_words
)
+from database import db
from utils import check_admin_status, delete_messages
from config import COMMAND_MESSAGES
+from action_reporter import action_reporter
logger = logging.getLogger(__name__)
@@ -139,6 +141,16 @@ async def add_bad_word(bot: AsyncTeleBot, message: Message, word: str):
reload_words() # Перезагружаем кэш
await send_temp_message(bot, message, f"✅ Слово '{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"Слово: {word}",
+ duration=None,
+ )
else:
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
@@ -158,6 +170,16 @@ async def remove_bad_word(bot: AsyncTeleBot, message: Message, word: str):
reload_words() # Перезагружаем кэш
await send_temp_message(bot, message, f"✅ Слово '{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"Слово: {word}",
+ duration=None,
+ )
else:
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
@@ -194,6 +216,16 @@ async def add_exception(bot: AsyncTeleBot, message: Message, word: str):
reload_words() # Перезагружаем кэш
await send_temp_message(bot, message, f"✅ Слово '{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"Слово: {word}",
+ duration=None,
+ )
else:
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
@@ -213,6 +245,16 @@ async def remove_exception(bot: AsyncTeleBot, message: Message, word: str):
reload_words() # Перезагружаем кэш
await send_temp_message(bot, message, f"✅ Слово '{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"Слово: {word}",
+ duration=None,
+ )
else:
await send_temp_message(bot, message, "❌ Ошибка при сохранении файла.")
diff --git a/src/modules/karma_commands.py b/src/modules/karma_commands.py
index 2d0222a..23a5107 100644
--- a/src/modules/karma_commands.py
+++ b/src/modules/karma_commands.py
@@ -4,6 +4,7 @@ import asyncio
import logging
from database import db
+from utils import check_admin_status, delete_messages
logger = logging.getLogger(__name__)
@@ -21,11 +22,14 @@ def register_handlers(bot: AsyncTeleBot):
@bot.message_handler(commands=['karma', 'rating'])
async def handle_karma_command(message: Message):
"""
- Команда /karma - показывает карму пользователя
+ Команда /karma - показывает карму пользователя или управляет настройками кармы
Использование:
/karma - показать свою карму
/karma @username - показать карму пользователя
/karma (в ответ на сообщение) - показать карму автора сообщения
+ /karma disable - отключить карму в топике (только админы)
+ /karma enable - включить карму в топике (только админы)
+ /karma status - показать статус кармы в топиках (только админы)
"""
try:
# Проверяем, что это групповой чат
@@ -34,6 +38,31 @@ def register_handlers(bot: AsyncTeleBot):
return
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_id = None
@@ -43,8 +72,8 @@ def register_handlers(bot: AsyncTeleBot):
target_user_id = target_user.id
# Если указан username в команде
- elif len(message.text.split()) > 1:
- username_arg = message.text.split()[1]
+ elif len(parts) > 1:
+ username_arg = parts[1]
# Убираем @ если есть
username = username_arg.lstrip('@')
@@ -136,9 +165,9 @@ def register_handlers(bot: AsyncTeleBot):
else:
medal = f"{idx}."
- # Формируем отображение пользователя
+ # Формируем отображение пользователя (без упоминаний)
if tag:
- user_display = f"@{tag}"
+ user_display = f"@{tag}"
else:
user_display = nickname or f"ID: {user_id}"
@@ -152,4 +181,69 @@ def register_handlers(bot: AsyncTeleBot):
except Exception as e:
logger.error(f"Ошибка при обработке команды /top: {e}", exc_info=True)
- await bot.reply_to(message, "❌ Произошла ошибка при получении топа")
\ No newline at end of file
+ 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"📊 Статус кармы в топиках\n\n"
+ text += f"🚫 Отключено в топиках (ID): {', '.join(map(str, disabled_topics))}\n"
+ text += f"\n💡 Используйте /karma enable в топике, чтобы включить карму"
+
+ 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)
\ No newline at end of file