diff --git a/bot/handlers/commands/users/listwords.py b/bot/handlers/commands/users/listwords.py new file mode 100644 index 0000000..4f99db3 --- /dev/null +++ b/bot/handlers/commands/users/listwords.py @@ -0,0 +1,243 @@ +""" +Обработчик команды /listwords - отображение всех правил модерации +""" +from aiogram import Router, F +from aiogram.filters import Command +from aiogram.types import Message, CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.exceptions import TelegramBadRequest + +from bot.filters.admin import IsAdmin +from configs import settings, COMMANDS +from database import get_manager +from middleware.loggers import logger +from bot.utils.decorators import log_action + +__all__ = ("router",) +CMD: str = "list" +router: Router = Router(name="listwords_cmd_router") + + +def get_refresh_kb(page: int = 0): + """Клавиатура с кнопкой обновления""" + ikb = InlineKeyboardBuilder() + ikb.button(text="🔄 Обновить", callback_data=f"listwords:refresh:{page}") + ikb.button(text="📊 Статистика", callback_data="stats") + ikb.adjust(2) + return ikb.as_markup() + + +async def format_banwords_list(page: int = 0) -> str: + """ + Форматирует список всех банвордов с разбивкой по типам. + + Args: + page: Номер страницы (для будущей пагинации) + + Returns: + Отформатированная строка со всеми правилами + """ + manager = get_manager() + + # Получаем все данные из БД + try: + # Используем существующий метод get_all_words_list() + data = await manager.get_all_words_list() + stats = await manager.get_stats() + + # Извлекаем данные из словаря + permanent_words = list(data.get('substring', set())) + permanent_lemmas = list(data.get('lemma', set())) + permanent_parts = list(data.get('part', set())) + temp_words = list(data.get('temp_substring', set())) + temp_lemmas = list(data.get('temp_lemma', set())) + conflict_words = list(data.get('conflict_substring', set())) + conflict_lemmas = list(data.get('conflict_lemma', set())) + exceptions = list(data.get('whitelist', set())) + + except Exception as e: + logger.error(f"Ошибка получения данных из БД: {e}", log_type="LISTWORDS") + return "❌ Ошибка загрузки данных из базы" + + # === ФОРМИРУЕМ ВЫВОД === + + output = "📋 СПИСОК ПРАВИЛ МОДЕРАЦИИ\n\n" + + # Статистика + total_count = ( + len(permanent_words) + len(permanent_lemmas) + len(permanent_parts) + + len(temp_words) + len(temp_lemmas) + + len(conflict_words) + len(conflict_lemmas) + ) + + output += f"📊 Общая статистика:\n" + output += f"├─ Всего правил: {total_count}\n" + output += f"├─ Исключений: {len(exceptions)}\n" + output += f"├─ Удалений за всё время: {stats.get('total_deletions', 0)}\n" + output += f"└─ Администраторов: {stats.get('admins', 0)}\n\n" + + # === ПОСТОЯННЫЕ ПРАВИЛА === + if permanent_words or permanent_lemmas or permanent_parts: + output += "🔴 ПОСТОЯННЫЕ ПРАВИЛА:\n\n" + + if permanent_words: + output += f"📝 Подстроки ({len(permanent_words)}):\n" + words_str = ', '.join([f"{w}" for w in sorted(permanent_words)[:20]]) + if len(permanent_words) > 20: + words_str += f" ... (+{len(permanent_words) - 20} ещё)" + output += f"{words_str}\n\n" + + if permanent_lemmas: + output += f"🔤 Леммы ({len(permanent_lemmas)}):\n" + lemmas_str = ', '.join([f"{w}" for w in sorted(permanent_lemmas)[:20]]) + if len(permanent_lemmas) > 20: + lemmas_str += f" ... (+{len(permanent_lemmas) - 20} ещё)" + output += f"{lemmas_str}\n\n" + + if permanent_parts: + output += f"🧩 Части ({len(permanent_parts)}):\n" + parts_str = ', '.join([f"{w}" for w in sorted(permanent_parts)[:20]]) + if len(permanent_parts) > 20: + parts_str += f" ... (+{len(permanent_parts) - 20} ещё)" + output += f"{parts_str}\n\n" + + # === ВРЕМЕННЫЕ ПРАВИЛА === + if temp_words or temp_lemmas: + output += "⏱ ВРЕМЕННЫЕ ПРАВИЛА:\n\n" + + if temp_words: + output += f"📝 Временные подстроки ({len(temp_words)}):\n" + # Для временных слов нужна дополнительная информация о времени истечения + # Пока просто выводим список + words_str = ', '.join([f"{w}" for w in sorted(temp_words)[:15]]) + if len(temp_words) > 15: + words_str += f" ... (+{len(temp_words) - 15} ещё)" + output += f"{words_str}\n\n" + + if temp_lemmas: + output += f"🔤 Временные леммы ({len(temp_lemmas)}):\n" + lemmas_str = ', '.join([f"{w}" for w in sorted(temp_lemmas)[:15]]) + if len(temp_lemmas) > 15: + lemmas_str += f" ... (+{len(temp_lemmas) - 15} ещё)" + output += f"{lemmas_str}\n\n" + + # === КОНФЛИКТНЫЕ ПРАВИЛА === + if conflict_words or conflict_lemmas: + output += "⚔️ КОНФЛИКТНЫЕ ПРАВИЛА:\n" + output += "(работают только в режиме /stopconflict время)\n\n" + + if conflict_words: + output += f"📝 Конфликтные слова ({len(conflict_words)}):\n" + words_str = ', '.join([f"{w}" for w in sorted(conflict_words)[:15]]) + if len(conflict_words) > 15: + words_str += f" ... (+{len(conflict_words) - 15} ещё)" + output += f"{words_str}\n\n" + + if conflict_lemmas: + output += f"🔤 Конфликтные леммы ({len(conflict_lemmas)}):\n" + lemmas_str = ', '.join([f"{w}" for w in sorted(conflict_lemmas)[:15]]) + if len(conflict_lemmas) > 15: + lemmas_str += f" ... (+{len(conflict_lemmas) - 15} ещё)" + output += f"{lemmas_str}\n\n" + + # === ИСКЛЮЧЕНИЯ (WHITELIST) === + if exceptions: + output += f"✅ ИСКЛЮЧЕНИЯ ({len(exceptions)}):\n" + exc_str = ', '.join([f"{exceptions}" for w in sorted(exceptions)[:15]]) + if len(exceptions) > 15: + exc_str += f" ... (+{len(exceptions) - 15} ещё)" + output += f"{exc_str}\n\n" + + # === АКТИВНЫЕ РЕЖИМЫ === + active_modes = [] + + if await manager.is_silence_active(): + active_modes.append("🔇 Режим тишины") + + if await manager.is_conflict_active(): + active_modes.append("⚔️ Режим антиконфликта") + + if active_modes: + output += "🔴 АКТИВНЫЕ РЕЖИМЫ:\n" + for mode in active_modes: + output += f"{mode}\n" + output += "\n" + + # === ПУСТОЙ СПИСОК === + if total_count == 0: + output = ( + "📋 СПИСОК ПРАВИЛ МОДЕРАЦИИ\n\n" + "⚠️ Правила модерации не настроены\n\n" + "Используйте команды добавления:\n" + "• /addword — добавить подстроку\n" + "• /addlemma — добавить лемму\n" + "• /addpart — добавить часть\n\n" + "📖 Подробнее: /start" + ) + + # Ограничение длины (Telegram limit 4096) + if len(output) > 4000: + output = output[:3950] + "\n\n... список обрезан, слишком много правил" + + return output + + +@router.callback_query(F.data.startswith("listwords:refresh")) +@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin()) +@log_action(action_name="LISTWORDS_COMMAND") +async def listwords_cmd(update: Message | CallbackQuery) -> None: + """ + Обработчик команды /listwords. + Отображает список всех правил модерации с разбивкой по категориям. + Доступно только администраторам. + Args: + update: Message или CallbackQuery + """ + # Определяем тип update + if isinstance(update, CallbackQuery): + message = update.message + is_callback = True + # Извлекаем номер страницы из callback_data + try: + page = int(update.data.split(":")[-1]) + except: + page = 0 + else: + message = update + is_callback = False + page = 0 + + # Формируем список + try: + text = await format_banwords_list(page) + keyboard = get_refresh_kb(page) + + if is_callback: + try: + await message.edit_text( + text=text, + parse_mode="HTML", + reply_markup=keyboard + ) + await update.answer("✅ Список обновлён") + except TelegramBadRequest as e: + if 'message is not modified' in str(e).lower(): + await update.answer('✅ Список уже актуален') + return + raise # Другие ошибки пробрасываем + else: + await message.answer( + text=text, + parse_mode="HTML", + reply_markup=keyboard + ) + + except Exception as e: + logger.error(f"Ошибка отправки списка банвордов: {e}", log_type="LISTWORDS") + + error_text = "❌ Ошибка загрузки списка\n\nПопробуйте позже" + + if is_callback: + await update.answer(f"❌ Ошибка загрузки: {e}", show_alert=True) + else: + await message.answer(error_text, parse_mode="HTML")