diff --git a/src/config.py b/src/config.py index 0d7df63..a3c27ef 100644 --- a/src/config.py +++ b/src/config.py @@ -22,6 +22,7 @@ COMMAND_MESSAGES = { "• /help - Этот справочник\n" "• /log - Инструкция по созданию логов\n\n" "🛠 Команды модерации:\n" + "• /warn help - Выдать предупреждение\n" "• /mute help - Инструкция по муту\n" "• /unmute help - Снятие мута\n" "• /ban help - Инструкция по бану\n" @@ -135,6 +136,25 @@ COMMAND_MESSAGES = { "3. По ID пользователя:\n" " /reset_violations 123456789\n\n" "ℹ️ Сбрасывает все записи об автомутах пользователя" - ) + ), + 'manual_warn': ( + "⚠️ Команда /warn\n\n" + "Выдает официальное предупреждение пользователю\n\n" + "🎯 Способы использования:\n" + "1. Ответ на сообщение:\n" + " /warn причина\n" + "2. По тегу пользователя:\n" + " /warn @username причина\n" + "3. По ID пользователя:\n" + " /warn 123456789 причина\n\n" + "📋 Система накопления:\n" + "• 1-й варн: просто предупреждение\n" + "• 2-й варн за неделю: автомут на сутки\n" + "• Повтор в течение 2 недель: мут на неделю\n\n" + "ℹ️ Причину обязательно указывайте для прозрачности" + ), + 'warned': '⚠️ Пользователь получил предупреждение.', + 'warned_auto_mute_day': '⚠️ Пользователь получил предупреждение и автомут на 1 день (повторное нарушение за неделю).', + 'warned_auto_mute_week': '⚠️ Пользователь получил предупреждение и автомут на 7 дней (множественные нарушения).' } \ No newline at end of file diff --git a/src/database.py b/src/database.py index 6c21a18..95822ae 100644 --- a/src/database.py +++ b/src/database.py @@ -34,6 +34,17 @@ class Database: # Инициализация класса FOREIGN KEY (user_id) REFERENCES users (id) ) ''') + cursor.execute(''' + CREATE TABLE IF NOT EXISTS warnings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + chat_id INTEGER NOT NULL, + warn_date INTEGER NOT NULL, + reason TEXT NOT NULL, + admin_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') connect.commit() # Возвращает соединение с базой данных @@ -156,5 +167,56 @@ class Database: # Инициализация класса logger.info(f"Сброшено {deleted_count} нарушений пользователя {user_id} в чате {chat_id}") return deleted_count + # Добавляет предупреждение в базу данных + 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(''' + INSERT INTO warnings (user_id, chat_id, warn_date, reason, admin_id) + VALUES (?, ?, ?, ?, ?) + ''', (user_id, chat_id, int(time.time()), reason, admin_id)) + connect.commit() + logger.info(f"Предупреждение пользователю {user_id} выдано администратором {admin_id} в чате {chat_id}") + + # Получает количество предупреждений за период + 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 + cursor.execute(''' + SELECT COUNT(*) + FROM warnings + WHERE user_id = ? AND chat_id = ? AND warn_date > ? + ''', (user_id, chat_id, cutoff_time)) + result = cursor.fetchone() + return result[0] if result else 0 + + # Получает все предупреждения пользователя + def get_user_warnings(self, user_id: int, chat_id: int): + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + SELECT id, warn_date, reason, admin_id + FROM warnings + WHERE user_id = ? AND chat_id = ? + ORDER BY warn_date DESC + ''', (user_id, chat_id)) + return cursor.fetchall() + + # Сбрасывает все предупреждения пользователя в чате + def reset_user_warnings(self, user_id: int, chat_id: int): + with self._get_connection() as connect: + cursor = connect.cursor() + cursor.execute(''' + DELETE FROM warnings + WHERE user_id = ? AND chat_id = ? + ''', (user_id, chat_id)) + deleted_count = cursor.rowcount + connect.commit() + logger.info(f"Сброшено {deleted_count} предупреждений пользователя {user_id} в чате {chat_id}") + return deleted_count + # Создаем экземпляр базы данных для импорта в других модулях db = Database() \ No newline at end of file diff --git a/src/main.py b/src/main.py index fe04c32..b891dfe 100644 --- a/src/main.py +++ b/src/main.py @@ -141,6 +141,7 @@ async def setup_bot_commands(): BotCommand("start", "Начало работы с ботом"), BotCommand("help", "Справка по всем командам"), BotCommand("log", "Инструкция по созданию лога ошибки"), + BotCommand("warn", "Выдать предупреждение. Использование: /warn help"), BotCommand("ban", "Забанить пользователя. Использование: /ban help"), BotCommand("unban", "Разбанить пользователя. Использование: /unban help"), BotCommand("mute", "Замутить пользователя. Использование: /mute help"), diff --git a/src/modules/warn.py b/src/modules/warn.py new file mode 100644 index 0000000..7b95d81 --- /dev/null +++ b/src/modules/warn.py @@ -0,0 +1,244 @@ +from telebot.async_telebot import AsyncTeleBot +from telebot.types import Message, User, ChatPermissions +import logging +import time + +from database import db +from action_reporter import action_reporter +from utils import ( + delete_messages, + check_admin_status, + check_target_status, +) + +from config import COMMAND_MESSAGES + +# Получаем логгер для текущего модуля +logger = logging.getLogger(__name__) + +# Регистрирует все обработчики команд +def register_handlers(bot: AsyncTeleBot): + + # Обработчик команды /warn + @bot.message_handler(commands=['warn']) + async def _warn_command_wrapper(message: Message): + await warn_command(bot, message) + +# Основная функция команды /warn +async def warn_command(bot: AsyncTeleBot, message: Message): + + # Определяем целевого пользователя + target_user = None + + # Определяем причину + reason = None + + # Разбиваем текст сообщения на части + parts_msg = message.text.split() + + # Команда /warn help + if len(parts_msg) == 2 and parts_msg[1].strip() in ('help', 'помощь'): + + # Отправляем инструкцию + await bot.send_message( + chat_id=message.chat.id, + text=COMMAND_MESSAGES['manual_warn'], + message_thread_id=message.message_thread_id, + ) + + # Удаляем сообщения через 30 секунд + await delete_messages(bot, message, time_sleep=30, number_message=2) + return + + try: + + # Проверяем, является ли отправитель администратором с правом ограничения + if await check_admin_status(bot, message) == 1: + return + + # Если одно слово (/warn) + if len(parts_msg) == 1: + + # Удаляем сообщение через 3 секунды + await delete_messages(bot, message, time_sleep=3, number_message=1) + return + + # Команда через ответ на сообщение, если два или более слов (/warn причина) + if message.reply_to_message and (not message.is_topic_message or message.message_thread_id != message.reply_to_message.message_id): + + # Собираем данные + target_user = message.reply_to_message.from_user + reason = ' '.join(parts_msg[1:]) if len(parts_msg) > 1 else 'отсутствует' + + # Если второе слово это тег или ID + elif len(parts_msg) >= 2 and (parts_msg[1].strip().isdigit() or parts_msg[1].startswith('@')): + + # Собираем данные + identifier = parts_msg[1].strip() + reason = ' '.join(parts_msg[2:]) if len(parts_msg) > 2 else 'отсутствует' + + # Делаем поиск по ID + if identifier.isdigit(): + + # Ищем пользователя в базе данных + user_info = db.get_user(int(identifier)) + + # Если нашли пользователя + if user_info: + + # Создаем объект пользователя + target_user = User( + id=user_info[0], + first_name=user_info[1], + username=user_info[2], + is_bot=False + ) + + # Делаем поиск по тегу + elif identifier.startswith('@'): + + # Ищем пользователя в базе данных (убрали @) + user_info = db.get_user_by_username(identifier[1:]) + + # Если нашли пользователя + if user_info: + + # Создаем объект пользователя + target_user = User( + id=user_info[0], + first_name=user_info[1], + username=user_info[2], + is_bot=False + ) + + # Если команда неправильная + else: + + # Удаляем сообщение через 3 секунды + await delete_messages(bot, message, time_sleep=3, number_message=1) + return + + # Если пользователь не найден в базе данных + if not target_user: + + # Отправляем предупреждение + await bot.send_message( + chat_id=message.chat.id, + text=COMMAND_MESSAGES['user_not_found'], + message_thread_id=message.message_thread_id, + ) + + # Удаляем сообщения через 5 секунд + await delete_messages(bot, message, time_sleep=5, number_message=2) + return + + # Проверяем статус целевого пользователя + if await check_target_status(bot, message, target_user) == 1: + return + + # Добавляем предупреждение в БД + db.add_warning( + user_id=target_user.id, + chat_id=message.chat.id, + reason=reason, + admin_id=message.from_user.id + ) + + # Проверяем количество предупреждений + ONE_WEEK = 604800 # 7 дней в секундах + TWO_WEEKS = 1209600 # 14 дней в секундах + + 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) + + logger.info(f"Предупреждений за неделю: {warns_week}, за 2 недели: {warns_two_weeks}") + + # Определяем, нужно ли применять мут + mute_applied = False + mute_duration = 0 + mute_duration_text = "" + response_message = COMMAND_MESSAGES['warned'] + + # Если это уже 2+ предупреждение за неделю -> мут на неделю + if warns_week >= 2: + mute_duration = 604800 # 7 дней + mute_duration_text = "7 дней" + response_message = COMMAND_MESSAGES['warned_auto_mute_week'] + mute_applied = True + logger.info(f"Применен мут на неделю (предупреждений за неделю: {warns_week})") + + # Если это 2-е предупреждение за 2 недели (но не за неделю) -> мут на сутки + elif warns_two_weeks >= 2: + mute_duration = 86400 # 1 день + mute_duration_text = "1 день" + response_message = COMMAND_MESSAGES['warned_auto_mute_day'] + mute_applied = True + logger.info(f"Применен мут на сутки (предупреждений за 2 недели: {warns_two_weeks})") + + # Применяем мут если нужно + if mute_applied: + try: + # Вычисляем время окончания мута + until_date = int(time.time()) + mute_duration + + # Устанавливаем ограничения (только чтение) + permissions = ChatPermissions( + can_send_messages=False, + can_send_media_messages=False, + can_send_polls=False, + can_send_other_messages=False, + can_add_web_page_previews=False, + can_change_info=False, + can_invite_users=False, + can_pin_messages=False, + ) + + # Выполняем мут + await bot.restrict_chat_member( + chat_id=message.chat.id, + user_id=target_user.id, + permissions=permissions, + until_date=until_date + ) + + logger.info(f"Пользователь {target_user.id} получил автомут на {mute_duration_text} после варна") + + except Exception as e: + logger.error(f"Ошибка при применении мута после варна: {str(e)}") + + # Отправляем сообщение-лог в админ-чат + await action_reporter.log_action( + action="ВАРН" if not mute_applied else f"ВАРН + МУТ ({mute_duration_text})", + user_id=target_user.id, + admin_id=message.from_user.id, + reason=reason, + duration=mute_duration_text if mute_applied else None, + ) + + # Отправляем сообщение в чат + await bot.send_message( + chat_id=message.chat.id, + text=response_message, + message_thread_id=message.message_thread_id, + ) + + # Записываем действие в логи + logger.info(f"Администратор {message.from_user.id} выдал предупреждение пользователю {target_user.id}. Причина: {reason}") + + # Удаляем сообщения через 5 секунд + await delete_messages(bot, message, time_sleep=5, number_message=2) + + except Exception as e: + + # Отправляем ошибку + await bot.send_message( + chat_id=message.chat.id, + text=COMMAND_MESSAGES['general_error'], + message_thread_id=message.message_thread_id, + ) + + # Записываем ошибку в логи + logger.error(f"Общая ошибка в warn_command: {str(e)}") + + # Удаляем сообщения через 5 секунд + await delete_messages(bot, message, time_sleep=5, number_message=2) \ No newline at end of file