diff --git a/bot/handlers/commands/users/conflict.py b/bot/handlers/commands/users/conflict.py new file mode 100644 index 0000000..8e75b95 --- /dev/null +++ b/bot/handlers/commands/users/conflict.py @@ -0,0 +1,435 @@ +""" +Обработчики команд режима антиконфликта +""" +from datetime import datetime +from aiogram import Router +from aiogram.filters import Command +from aiogram.types import Message + +from bot.filters.admin import IsAdmin +from configs import settings, COMMANDS +from database import get_manager +from database.models import BanWordType +from middleware.loggers import logger +from bot.utils.decorators import log_action + +__all__ = ("router",) + +router: Router = Router(name="conflict_mode_router") + + +# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ================= + +def parse_conflict_args(text: str, command: str, need_minutes: bool = False) -> tuple[bool, str | list]: + """ + Парсит аргументы команды для конфликтного режима. + + Args: + text: Полный текст сообщения + command: Название команды + need_minutes: Требуется ли параметр минут + + Returns: + (success, result): result это либо список аргументов, либо текст ошибки + """ + parts = text.split(maxsplit=2 if need_minutes else 1) + + min_args = 1 if need_minutes else 1 + + if len(parts) < min_args + 1: + if need_minutes: + return False, f"❌ Использование: /{command} [минуты]" + else: + return False, f"❌ Использование: /{command} [слово]" + + args = parts[1:] + + # Валидация слова + if not need_minutes: + if len(args[0]) < 2: + return False, "❌ Слово должно содержать минимум 2 символа" + + if len(args[0]) > 100: + return False, "❌ Слово слишком длинное (максимум 100 символов)" + + return True, args + + +def format_time_str(minutes: int) -> str: + """Форматирует время в читабельный формат""" + if minutes < 60: + return f"{minutes} мин" + elif minutes < 1440: + hours = minutes // 60 + mins = minutes % 60 + return f"{hours}ч {mins}м" if mins else f"{hours}ч" + else: + days = minutes // 1440 + hours = (minutes % 1440) // 60 + return f"{days}д {hours}ч" if hours else f"{days}д" + + +def format_datetime(dt: datetime) -> str: + """Форматирует datetime в читабельный формат""" + return dt.strftime("%d.%m.%Y %H:%M:%S") + + +# ================= ДОБАВЛЕНИЕ КОНФЛИКТНЫХ СЛОВ ================= + +@router.message( + Command(*COMMANDS.get("addconflictword", ["addconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin()) +@log_action(action_name="ADD_CONFLICT_WORD", log_args=True) +async def add_conflict_word_cmd(message: Message) -> None: + """ + Добавляет конфликтное слово-подстроку. + + Конфликтные слова работают только в режиме /stopconflict. + + Использование: /addconflictword <слово> + """ + success, result = parse_conflict_args(message.text, "addconflictword", need_minutes=False) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + added = await manager.add_banword( + word=word, + word_type=BanWordType.CONFLICT_SUBSTRING, + added_by=message.from_user.id, + reason="Конфликтное слово" + ) + + if added: + text = ( + f"✅ Конфликтное слово добавлено\n\n" + f"📝 Слово: {word}\n" + f"🔍 Тип: подстрока\n\n" + f"⚔️ Будет работать только в режиме антиконфликта\n" + f"Активируйте: /stopconflict [минуты]" + ) + else: + text = f"⚠️ Конфликтное слово {word} уже существует" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error(f"Ошибка добавления конфликтного слова: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") + + +@router.message( + Command(*COMMANDS.get("addconflictlemma", ["addconflictlemma"]), prefix=settings.PREFIX, ignore_case=True), + IsAdmin()) +@log_action(action_name="ADD_CONFLICT_LEMMA", log_args=True) +async def add_conflict_lemma_cmd(message: Message) -> None: + """ + Добавляет конфликтную лемму. + + Конфликтные леммы работают только в режиме /stopconflict. + + Использование: /addconflictlemma <слово> + """ + success, result = parse_conflict_args(message.text, "addconflictlemma", need_minutes=False) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + added = await manager.add_banword( + word=word, + word_type=BanWordType.CONFLICT_LEMMA, + added_by=message.from_user.id, + reason="Конфликтная лемма" + ) + + if added: + text = ( + f"✅ Конфликтная лемма добавлена\n\n" + f"🔤 Слово: {word}\n" + f"🔍 Тип: лемма (все формы слова)\n\n" + f"⚔️ Будет работать только в режиме антиконфликта\n" + f"Активируйте: /stopconflict [минуты]" + ) + else: + text = f"⚠️ Конфликтная лемма {word} уже существует" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error(f"Ошибка добавления конфликтной леммы: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка добавления\n\nПопробуйте позже", parse_mode="HTML") + + +# ================= УДАЛЕНИЕ КОНФЛИКТНЫХ СЛОВ ================= + +@router.message( + Command(*COMMANDS.get("remconflictword", ["remconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin()) +@log_action(action_name="REMOVE_CONFLICT_WORD", log_args=True) +async def remove_conflict_word_cmd(message: Message) -> None: + """ + Удаляет конфликтное слово-подстроку. + + Использование: /remconflictword <слово> + """ + success, result = parse_conflict_args(message.text, "remconflictword", need_minutes=False) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + removed = await manager.remove_banword( + word=word, + word_type=BanWordType.CONFLICT_SUBSTRING + ) + + if removed: + text = f"🗑 Конфликтное слово удалено\n\n📝 Слово: {word}" + else: + text = f"⚠️ Конфликтное слово {word} не найдено" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error(f"Ошибка удаления конфликтного слова: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") + + +@router.message( + Command(*COMMANDS.get("remconflictlemma", ["remconflictlemma"]), prefix=settings.PREFIX, ignore_case=True), + IsAdmin()) +@log_action(action_name="REMOVE_CONFLICT_LEMMA", log_args=True) +async def remove_conflict_lemma_cmd(message: Message) -> None: + """ + Удаляет конфликтную лемму. + + Использование: /remconflictlemma <слово> + """ + success, result = parse_conflict_args(message.text, "remconflictlemma", need_minutes=False) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + word = result[0].lower().strip() + manager = get_manager() + + try: + removed = await manager.remove_banword( + word=word, + word_type=BanWordType.CONFLICT_LEMMA + ) + + if removed: + text = f"🗑 Конфликтная лемма удалена\n\n🔤 Слово: {word}" + else: + text = f"⚠️ Конфликтная лемма {word} не найдена" + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error(f"Ошибка удаления конфликтной леммы: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка удаления\n\nПопробуйте позже", parse_mode="HTML") + + +# ================= УПРАВЛЕНИЕ РЕЖИМОМ АНТИКОНФЛИКТА ================= + +@router.message(Command(*COMMANDS.get("stopconflict", ["stopconflict"]), prefix=settings.PREFIX, ignore_case=True), + IsAdmin()) +@log_action(action_name="START_CONFLICT_MODE", log_args=True) +async def start_conflict_mode_cmd(message: Message) -> None: + """ + Активирует режим антиконфликта на указанное время. + + В этом режиме работают только конфликтные слова/леммы. + Обычные банворды временно отключаются. + + Использование: /stopconflict <минуты> + Пример: /stopconflict 30 + """ + success, result = parse_conflict_args(message.text, "stopconflict", need_minutes=True) + + if not success: + await message.answer(result, parse_mode="HTML") + return + + # Валидация минут + try: + minutes = int(result[0]) + if minutes < 1 or minutes > 10080: # Максимум неделя + await message.answer( + "❌ Время должно быть от 1 минуты до 10080 минут (7 дней)", + parse_mode="HTML" + ) + return + except ValueError: + await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML") + return + + manager = get_manager() + + try: + # Получаем статистику конфликтных слов + data = await manager.get_all_words_list() + conflict_words_count = len(data.get('conflict_substring', set())) + conflict_lemmas_count = len(data.get('conflict_lemma', set())) + total_conflict = conflict_words_count + conflict_lemmas_count + + if total_conflict == 0: + await message.answer( + "⚠️ Нет конфликтных слов\n\n" + "Сначала добавьте конфликтные слова:\n" + "• /addconflictword [слово]\n" + "• /addconflictlemma [слово]", + parse_mode="HTML" + ) + return + + # Активируем режим + expires_at = await manager.set_conflict_mode(minutes) + + time_str = format_time_str(minutes) + expires_str = format_datetime(expires_at) + + text = ( + f"⚔️ РЕЖИМ АНТИКОНФЛИКТА АКТИВИРОВАН\n\n" + f"⏱ Длительность: {time_str}\n" + f"🕐 Окончание: {expires_str}\n\n" + f"📊 Активные правила:\n" + f"├─ Конфликтные слова: {conflict_words_count}\n" + f"└─ Конфликтные леммы: {conflict_lemmas_count}\n\n" + f"⚠️ Обычные банворды временно отключены\n" + f"Отключить режим: /unstopconflict" + ) + + await message.answer(text, parse_mode="HTML") + + logger.info( + f"Режим антиконфликта активирован на {minutes} мин " + f"(конфликтных правил: {total_conflict})", + log_type="CONFLICT" + ) + + except Exception as e: + logger.error(f"Ошибка активации режима антиконфликта: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка активации режима\n\nПопробуйте позже", parse_mode="HTML") + + +@router.message(Command(*COMMANDS.get("unstopconflict", ["unstopconflict"]), prefix=settings.PREFIX, ignore_case=True), + IsAdmin()) +@log_action(action_name="STOP_CONFLICT_MODE") +async def stop_conflict_mode_cmd(message: Message) -> None: + """ + Отключает режим антиконфликта. + + Использование: /unstopconflict + """ + manager = get_manager() + + try: + # Проверяем, активен ли режим + is_active = await manager.is_conflict_active() + + if not is_active: + await message.answer( + "⚠️ Режим антиконфликта не активен\n\n" + "Активируйте: /stopconflict [минуты]", + parse_mode="HTML" + ) + return + + # Отключаем режим + await manager.disable_conflict_mode() + + text = ( + f"✅ Режим антиконфликта отключен\n\n" + f"🔄 Обычные банворды снова активны\n" + f"⚔️ Конфликтные слова деактивированы" + ) + + await message.answer(text, parse_mode="HTML") + + logger.info("Режим антиконфликта отключён", log_type="CONFLICT") + + except Exception as e: + logger.error(f"Ошибка отключения режима антиконфликта: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка отключения режима\n\nПопробуйте позже", parse_mode="HTML") + + +@router.message(Command(*COMMANDS.get("conflictstatus", ["conflictstatus"]), prefix=settings.PREFIX, ignore_case=True), + IsAdmin()) +@log_action(action_name="CONFLICT_STATUS") +async def conflict_status_cmd(message: Message) -> None: + """ + Показывает статус режима антиконфликта. + + Использование: /conflictstatus + """ + manager = get_manager() + + try: + # Проверяем активность режима + is_active = await manager.is_conflict_active() + + # Получаем статистику + data = await manager.get_all_words_list() + conflict_words_count = len(data.get('conflict_substring', set())) + conflict_lemmas_count = len(data.get('conflict_lemma', set())) + total_conflict = conflict_words_count + conflict_lemmas_count + + if is_active: + # Режим активен - показываем детали + conflict_until_str = await manager.repo.get_setting("conflict_until") + conflict_until = float(conflict_until_str) + expires_at = datetime.fromtimestamp(conflict_until) + + now = datetime.now() + time_left_seconds = (expires_at - now).total_seconds() + time_left_minutes = int(time_left_seconds / 60) + + text = ( + f"⚔️ РЕЖИМ АНТИКОНФЛИКТА АКТИВЕН\n\n" + f"⏱ Осталось: {format_time_str(time_left_minutes)}\n" + f"🕐 Окончание: {format_datetime(expires_at)}\n\n" + f"📊 Активные правила:\n" + f"├─ Конфликтные слова: {conflict_words_count}\n" + f"└─ Конфликтные леммы: {conflict_lemmas_count}\n\n" + f"⚠️ Обычные банворды отключены\n" + f"Отключить: /unstopconflict" + ) + else: + # Режим не активен + text = ( + f"💤 Режим антиконфликта НЕ активен\n\n" + f"📊 Конфликтных правил в базе:\n" + f"├─ Слова: {conflict_words_count}\n" + f"└─ Леммы: {conflict_lemmas_count}\n\n" + ) + + if total_conflict > 0: + text += f"Активировать: /stopconflict [минуты]" + else: + text += ( + f"⚠️ Нет конфликтных слов\n" + f"Добавьте:\n" + f"• /addconflictword [слово]\n" + f"• /addconflictlemma [слово]" + ) + + await message.answer(text, parse_mode="HTML") + + except Exception as e: + logger.error(f"Ошибка получения статуса режима: {e}", log_type="CONFLICT") + await message.answer("❌ Ошибка получения статуса", parse_mode="HTML")