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")