diff --git a/.gigaide/gigaide.properties b/.gigaide/gigaide.properties index 064b3d2..6ef783e 100644 --- a/.gigaide/gigaide.properties +++ b/.gigaide/gigaide.properties @@ -1,5 +1,5 @@ -## changed at Sat Oct 18 12:59:47 MSK 2025 -#Sat Oct 18 12:59:47 MSK 2025 +## changed at Sun Oct 19 12:21:52 MSK 2025 +#Sun Oct 19 12:21:52 MSK 2025 com.gigaide.elements.ext.marker.solution.BeanMarkedPsi.shouldMark=true com.gigaide.elements.ext.marker.solution.ConfigMarkedPsi.shouldMark=true com.gigaide.elements.ext.marker.solution.DataMarkedPsi.shouldMark=true diff --git a/src/action_reporter.py b/src/action_reporter.py index 0811318..eceeeb6 100644 --- a/src/action_reporter.py +++ b/src/action_reporter.py @@ -1,5 +1,6 @@ from telebot.async_telebot import AsyncTeleBot from telebot.types import Message +from typing import Optional import logging import os from database import db @@ -33,7 +34,7 @@ class ActionReporter: return text # Получает информацию об администраторе - async def _get_admin_info(self, admin_id: int | None) -> str: + async def _get_admin_info(self, admin_id: Optional[int]) -> str: # Если админ не указан (автоматическое действие) if admin_id is None: return "🤖 Администратор: Автоматическое действие" @@ -58,7 +59,7 @@ class ActionReporter: return text # Отправляет лог действия в админ-чат - async def log_action(self, action: str, user_id: int, admin_id: int | None, reason: str, duration: str, photo_path: str = None): + async def log_action(self, action: str, user_id: int, admin_id: Optional[int], reason: str, duration: str, photo_path: Optional[str] = None): try: # Получаем информацию о пользователе и администраторе diff --git a/src/bad_words.py b/src/bad_words.py index 9684429..cc59c56 100644 --- a/src/bad_words.py +++ b/src/bad_words.py @@ -116,15 +116,37 @@ def contains_bad_word(text: str) -> bool: bad_words = get_bad_words() exceptions = get_exceptions() - # Проверяем исключения - for exception in exceptions: - if exception in text_lower: - text_lower = text_lower.replace(exception, '') - # Проверяем бранные слова for bad_word in bad_words: if bad_word in text_lower: - return True + # Проверяем, не является ли это слово частью исключения + # Ищем все вхождения плохого слова + start = 0 + while True: + pos = text_lower.find(bad_word, start) + if pos == -1: + break + + # Проверяем, входит ли это вхождение в какое-либо исключение + is_exception = False + for exception in exceptions: + # Проверяем, находится ли плохое слово внутри слова-исключения + # и содержится ли это слово-исключение в тексте в этой позиции + if bad_word in exception: + # Ищем позицию исключения, которое могло бы содержать это плохое слово + exc_start = text_lower.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 @@ -148,14 +170,37 @@ def get_bad_words_from_text(text: str) -> list: bad_words = get_bad_words() exceptions = get_exceptions() - # Проверяем исключения - for exception in exceptions: - if exception in text_lower: - text_lower = text_lower.replace(exception, '') - # Ищем бранные слова for bad_word in bad_words: if bad_word in text_lower: - found_words.append(bad_word) + # Проверяем, не является ли это слово частью исключения + start = 0 + word_is_valid = False + while True: + pos = text_lower.find(bad_word, start) + if pos == -1: + break + + # Проверяем, входит ли это вхождение в какое-либо исключение + is_exception = False + for exception in exceptions: + if bad_word in exception: + exc_start = text_lower.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: + found_words.append(bad_word) return found_words \ No newline at end of file diff --git a/src/config.py b/src/config.py index b46f6ec..a7c28c3 100644 --- a/src/config.py +++ b/src/config.py @@ -7,6 +7,23 @@ DATABASE_NAME = 'users.db' # Название файла для логов LOG_FILE_NAME = 'bot.log' +# =========================================== +# Временные константы (в секундах) +# =========================================== + +# Период учёта нарушений (30 дней) +VIOLATIONS_PERIOD = 2592000 + +# Кулдаун для благодарностей (1 час) +THANK_COOLDOWN = 3600 + +# Периоды для предупреждений +ONE_WEEK = 604800 # 7 дней +TWO_WEEKS = 1209600 # 14 дней + +# Максимальное время мута (30 дней) +MAX_MUTE_TIME = 2592000 + # Сообщения команд COMMAND_MESSAGES = { 'start': 'Бот-администратор для чата @linux_gaming_ru', @@ -152,13 +169,13 @@ COMMAND_MESSAGES = { " /warn 123456789 причина\n\n" "📋 Система накопления:\n" "• 1-й варн: просто предупреждение\n" - "• 2-й варн за неделю: автомут на сутки\n" - "• Повтор в течение 2 недель: мут на неделю\n\n" + "• 2-й варн за неделю: автомут на 7 дней (строгое)\n" + "• 2-й варн за 2 недели: автомут на 1 день (мягкое)\n\n" "ℹ️ Причину обязательно указывайте для прозрачности" ), 'warned': '⚠️ Пользователь получил предупреждение.', - 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (повторное нарушение за неделю).', - 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (множественные нарушения).', + 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (2-е предупреждение за 2 недели).', + 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (2-е предупреждение за неделю - строгое наказание).', 'karma_help': ( "⭐ Команда /karma\n\n" "Показывает карму пользователя в этом чате\n\n" diff --git a/src/database.py b/src/database.py index b6ed952..6fb4837 100644 --- a/src/database.py +++ b/src/database.py @@ -1,5 +1,6 @@ import sqlite3 import os +import time from typing import Optional, Tuple import logging @@ -65,7 +66,34 @@ class Database: # Инициализация класса FOREIGN KEY (to_user_id) REFERENCES users (id) ) ''') + + # Создаём индексы для оптимизации часто используемых запросов + # Индекс для проверки нарушений пользователя в чате за период + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_violations_user_chat_date + ON violations(user_id, chat_id, violation_date) + ''') + + # Индекс для проверки предупреждений пользователя в чате за период + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_warnings_user_chat_date + ON warnings(user_id, chat_id, warn_date) + ''') + + # Индекс для проверки истории кармы (кулдаун благодарностей) + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_karma_history_cooldown + ON karma_history(from_user_id, to_user_id, chat_id, timestamp) + ''') + + # Индекс для поиска пользователя по username + cursor.execute(''' + CREATE INDEX IF NOT EXISTS idx_users_tag + ON users(tag COLLATE NOCASE) + ''') + connect.commit() + logger.info("База данных и индексы успешно инициализированы") # Возвращает соединение с базой данных def _get_connection(self): @@ -123,7 +151,6 @@ class Database: # Инициализация класса # Добавляет нарушение в базу данных def add_violation(self, user_id: int, chat_id: int, violation_type: str = 'bad_language'): - import time with self._get_connection() as connect: cursor = connect.cursor() cursor.execute(''' @@ -135,7 +162,6 @@ class Database: # Инициализация класса # Получает количество нарушений за период (по умолчанию - за последний месяц) def get_violations_count(self, user_id: int, chat_id: int, period_seconds: int = 2592000) -> int: - import time with self._get_connection() as connect: cursor = connect.cursor() cutoff_time = int(time.time()) - period_seconds @@ -161,7 +187,6 @@ class Database: # Инициализация класса # Очищает старые нарушения (старше указанного периода) def clean_old_violations(self, period_seconds: int = 2592000): - import time with self._get_connection() as connect: cursor = connect.cursor() cutoff_time = int(time.time()) - period_seconds @@ -189,7 +214,6 @@ class Database: # Инициализация класса # Добавляет предупреждение в базу данных def add_warning(self, user_id: int, chat_id: int, reason: str, admin_id: int): - import time with self._get_connection() as connect: cursor = connect.cursor() cursor.execute(''' @@ -201,7 +225,6 @@ class Database: # Инициализация класса # Получает количество предупреждений за период def get_warnings_count(self, user_id: int, chat_id: int, period_seconds: int) -> int: - import time with self._get_connection() as connect: cursor = connect.cursor() cutoff_time = int(time.time()) - period_seconds @@ -290,9 +313,40 @@ class Database: # Инициализация класса ''', (chat_id, limit)) return cursor.fetchall() - # Проверяет, может ли пользователь поблагодарить другого (проверка кулдауна) + # Атомарно проверяет кулдаун и добавляет запись в историю кармы + # Возвращает True если благодарность засчитана, False если кулдаун не прошёл + def try_add_karma_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool: + with self._get_connection() as connect: + cursor = connect.cursor() + current_time = int(time.time()) + cutoff_time = current_time - cooldown_seconds + + # Проверяем кулдаун и добавляем запись в одной транзакции + # Это предотвращает race condition при параллельных запросах + cursor.execute(''' + SELECT COUNT(*) + FROM karma_history + WHERE from_user_id = ? AND to_user_id = ? AND chat_id = ? AND timestamp > ? + ''', (from_user_id, to_user_id, chat_id, cutoff_time)) + result = cursor.fetchone() + + # Если кулдаун не прошёл, возвращаем False + if result and result[0] > 0: + return False + + # Кулдаун прошёл, добавляем запись в той же транзакции + cursor.execute(''' + INSERT INTO karma_history (from_user_id, to_user_id, chat_id, timestamp) + VALUES (?, ?, ?, ?) + ''', (from_user_id, to_user_id, chat_id, current_time)) + + connect.commit() + logger.info(f"Пользователь {from_user_id} поблагодарил {to_user_id} в чате {chat_id}") + return True + + # УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости + # Используйте try_add_karma_thank() для атомарной операции без race condition def can_thank(self, from_user_id: int, to_user_id: int, chat_id: int, cooldown_seconds: int = 3600) -> bool: - import time with self._get_connection() as connect: cursor = connect.cursor() cutoff_time = int(time.time()) - cooldown_seconds @@ -304,9 +358,9 @@ class Database: # Инициализация класса result = cursor.fetchone() return result[0] == 0 if result else True - # Добавляет запись в историю кармы + # УСТАРЕВШИЙ МЕТОД - оставлен для обратной совместимости + # Используйте try_add_karma_thank() для атомарной операции без race condition def add_karma_history(self, from_user_id: int, to_user_id: int, chat_id: int): - import time with self._get_connection() as connect: cursor = connect.cursor() cursor.execute(''' diff --git a/src/logger.py b/src/logger.py index 8832e08..b28beb8 100644 --- a/src/logger.py +++ b/src/logger.py @@ -52,7 +52,7 @@ def setup_logging(): # Инициализирует систему логиро # Создаем корневой логгер logger = logging.getLogger() - logger.setLevel(logging.DEBUG) # Временно DEBUG для отладки + logger.setLevel(logging.INFO) # INFO для продакшена # Проверяем, не настроен ли логгер ранее if not logger.hasHandlers(): diff --git a/src/main.py b/src/main.py index 01ba25c..dd5c741 100644 --- a/src/main.py +++ b/src/main.py @@ -47,7 +47,7 @@ validate_env_vars() bot = AsyncTeleBot(os.getenv("BOT_TOKEN"), parse_mode="html") # Загружаем ID админ-чата из .env и инициализируемся для логов в чат -init_action_reporter(bot, os.getenv("ADMIN_CHAT_ID"), os.getenv("LOG_THREAD_ID")) +init_action_reporter(bot, int(os.getenv("ADMIN_CHAT_ID")), int(os.getenv("LOG_THREAD_ID"))) # Получаем логгер для текущего модуля logger = logging.getLogger(__name__) diff --git a/src/modules/0_karma_tracker.py b/src/modules/0_karma_tracker.py index 0a979a9..bee08d9 100644 --- a/src/modules/0_karma_tracker.py +++ b/src/modules/0_karma_tracker.py @@ -1,16 +1,15 @@ from telebot.async_telebot import AsyncTeleBot from telebot.types import Message +import asyncio import logging from database import db from thank_words import contains_thank_word from bad_words import contains_bad_word +from config import THANK_COOLDOWN logger = logging.getLogger(__name__) -# Кулдаун для благодарностей (в секундах) - 1 час -THANK_COOLDOWN = 3600 - def register_handlers(bot: AsyncTeleBot): """Регистрирует обработчики для отслеживания благодарностей""" logger.info("Регистрация обработчика благодарностей (karma_tracker)") @@ -55,15 +54,15 @@ def register_handlers(bot: AsyncTeleBot): logger.info(f"Пользователь {from_user.id} попытался поблагодарить бота") return - # Проверяем кулдаун (можно благодарить одного пользователя раз в час) - if not db.can_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN): + # Атомарно проверяем кулдаун и записываем благодарность + # Это предотвращает race condition при параллельных запросах + if not db.try_add_karma_thank(from_user.id, to_user.id, chat_id, THANK_COOLDOWN): logger.info(f"Пользователь {from_user.id} уже благодарил {to_user.id} недавно") # Молча игнорируем, чтобы не спамить return - # Начисляем карму + # Начисляем карму (благодарность уже записана атомарно выше) db.add_karma(to_user.id, chat_id, 1) - db.add_karma_history(from_user.id, to_user.id, chat_id) # Получаем новую карму пользователя new_karma = db.get_karma(to_user.id, chat_id) @@ -83,7 +82,6 @@ def register_handlers(bot: AsyncTeleBot): logger.info(f"Пользователь {from_user.id} поблагодарил {to_user.id}, карма: {new_karma}") # Удаляем уведомление через 5 секунд - import asyncio await asyncio.sleep(5) try: await bot.delete_message(chat_id, sent_message.message_id) diff --git a/src/modules/auto_mute.py b/src/modules/auto_mute.py index 154688e..4fda437 100644 --- a/src/modules/auto_mute.py +++ b/src/modules/auto_mute.py @@ -8,6 +8,7 @@ from database import db from bad_words import contains_bad_word, get_bad_words_from_text from action_reporter import action_reporter from utils import delete_messages, format_mute_time +from config import VIOLATIONS_PERIOD # Получаем логгер для текущего модуля logger = logging.getLogger(__name__) @@ -31,9 +32,6 @@ MUTE_LEVELS = [ None, # 14. Перманентный мут (режим только чтения навсегда) ] -# Период для подсчета нарушений (30 дней в секундах) -VIOLATIONS_PERIOD = 2592000 - def get_mute_duration(violations_count: int) -> int: """ Определяет длительность мута на основе количества нарушений. diff --git a/src/modules/karma_commands.py b/src/modules/karma_commands.py index 2fdb3a6..02311d7 100644 --- a/src/modules/karma_commands.py +++ b/src/modules/karma_commands.py @@ -1,5 +1,6 @@ from telebot.async_telebot import AsyncTeleBot from telebot.types import Message +import asyncio import logging from database import db @@ -8,7 +9,6 @@ logger = logging.getLogger(__name__) async def _delete_message_delayed(bot: AsyncTeleBot, chat_id: int, message_id: int, delay: int): """Удаляет сообщение с задержкой""" - import asyncio try: await asyncio.sleep(delay) await bot.delete_message(chat_id, message_id) @@ -94,7 +94,6 @@ def register_handlers(bot: AsyncTeleBot): sent_message = await bot.reply_to(message, response) # Удаляем команду через 20 секунд и ответ через 60 секунд - import asyncio asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20)) asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60)) @@ -142,10 +141,9 @@ def register_handlers(bot: AsyncTeleBot): response += f"{medal} {user_display} — {karma_points} кармы\n" - sent_message = await bot.reply_to(message, response, parse_mode='HTML') + sent_message = await bot.reply_to(message, response) # Удаляем команду через 20 секунд и ответ через 60 секунд - import asyncio asyncio.create_task(_delete_message_delayed(bot, chat_id, message.message_id, 20)) asyncio.create_task(_delete_message_delayed(bot, chat_id, sent_message.message_id, 60)) diff --git a/src/modules/mute.py b/src/modules/mute.py index da72c01..0ba6551 100644 --- a/src/modules/mute.py +++ b/src/modules/mute.py @@ -33,7 +33,7 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No # Определяем целевого пользователя target_user = None - # Отпределяем время + # Определяем время time_arg = None # Определяем причину @@ -233,8 +233,11 @@ async def mute_command(bot: AsyncTeleBot, message: Message, photo_path: str = No await delete_messages(bot, message, time_sleep=5, number_message=2) return - # Максимальное время мута - 30 дней (2592000 секунд) - if mute_seconds > 2592000: + # Импортируем максимальное время мута + from config import MAX_MUTE_TIME + + # Максимальное время мута - 30 дней + if mute_seconds > MAX_MUTE_TIME: # Отправляем предупреждение await bot.send_message( diff --git a/src/modules/warn.py b/src/modules/warn.py index 7b95d81..203cfdc 100644 --- a/src/modules/warn.py +++ b/src/modules/warn.py @@ -144,10 +144,10 @@ async def warn_command(bot: AsyncTeleBot, message: Message): admin_id=message.from_user.id ) - # Проверяем количество предупреждений - ONE_WEEK = 604800 # 7 дней в секундах - TWO_WEEKS = 1209600 # 14 дней в секундах + # Импортируем константы времени + from config import ONE_WEEK, TWO_WEEKS + # Проверяем количество предупреждений warns_week = db.get_warnings_count(target_user.id, message.chat.id, ONE_WEEK) warns_two_weeks = db.get_warnings_count(target_user.id, message.chat.id, TWO_WEEKS)