From f317650c5fe1c9cf1ca36264c67d91c7d9364c5b Mon Sep 17 00:00:00 2001 From: Verum Date: Mon, 23 Feb 2026 14:18:46 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20/settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/handlers/commands/users/bot_settings.py | 330 ++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 bot/handlers/commands/users/bot_settings.py diff --git a/bot/handlers/commands/users/bot_settings.py b/bot/handlers/commands/users/bot_settings.py new file mode 100644 index 0000000..23edb71 --- /dev/null +++ b/bot/handlers/commands/users/bot_settings.py @@ -0,0 +1,330 @@ +""" +Команда /settings - управление настройками БЕЗ .env +ADMIN_CHAT_ID, ADMIN_THREAD_ID, REPORT_CHAT_ID, REPORT_THREAD_ID +""" + +from aiogram import Router, F +from aiogram.types import Message, CallbackQuery +from aiogram.filters import Command +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.exceptions import TelegramBadRequest + +from middleware.loggers import logger +from bot.filters.admin import IsAdmin +from database import get_manager + +__all__ = ("router",) + +router: Router = Router(name="bot_settings_router") + +# ====================================================================== +# FSM STATES +# ====================================================================== + +class BotSettingsStates(StatesGroup): + """Состояния для редактирования настроек бота""" + waiting_admin_chat = State() + waiting_admin_thread = State() + waiting_report_chat = State() + waiting_report_thread = State() + +# ====================================================================== +# MAIN MENU +# ====================================================================== + +def _format_chat_id(chat_id: str | None) -> str: + """Форматирует ID чата для отображения""" + if chat_id is None: + return "❌ Не установлен" + return f"✅ {chat_id}" + +def create_settings_menu() -> InlineKeyboardBuilder: + """Главное меню настроек""" + ikb = InlineKeyboardBuilder() + ikb.button(text="📢 Админ-чат", callback_data="settings:admin_chat") + ikb.button(text="🧵 Топик админ-чата", callback_data="settings:admin_thread") + ikb.button(text="📊 Чат репортов", callback_data="settings:report_chat") + ikb.button(text="🧵 Топик репортов", callback_data="settings:report_thread") + ikb.button(text="🔄 Обновить", callback_data="settings:refresh") + ikb.button(text="❌ Закрыть", callback_data="settings:close") + ikb.adjust(2) + return ikb + +def cancel_keyboard(): + """Клавиатура с кнопкой 'Назад' для окон ввода""" + ikb = InlineKeyboardBuilder() + ikb.button(text="◀️ Назад", callback_data="settings:cancel") + return ikb.as_markup() + +# ====================================================================== +# MAIN HANDLER +# ====================================================================== + +@router.message(Command("settings"), IsAdmin()) +async def settings_cmd(message: Message, state: FSMContext) -> None: + """Главная команда /settings""" + await state.clear() + await show_settings_menu(message) + +async def show_settings_menu(message_or_callback: Message | CallbackQuery) -> None: + """Показывает меню настроек (отправляет новое сообщение или редактирует существующее)""" + manager = get_manager() + current = await manager.get_bot_settings() + + text = ( + "⚙️ НАСТРОЙКИ БОТА\n\n" + "📢 Админ-чат: " + _format_chat_id(current.get('admin_chat_id')) + "\n" + "🧵 Топик админ: " + _format_chat_id(current.get('admin_thread_id')) + "\n\n" + "📊 Чат репортов: " + _format_chat_id(current.get('report_chat_id')) + "\n" + "🧵 Топик репортов: " + _format_chat_id(current.get('report_thread_id')) + "\n\n" + "💡 Используйте @userinfobot для получения ID чатов\n" + "💡 Для топиков: ID из сообщения в топике" + ) + + markup = create_settings_menu().as_markup() + + if isinstance(message_or_callback, Message): + await message_or_callback.answer(text, reply_markup=markup, parse_mode="HTML") + else: + try: + await message_or_callback.message.edit_text(text, reply_markup=markup, parse_mode="HTML") + except TelegramBadRequest as e: + if "message is not modified" in str(e): + await message_or_callback.answer("🔄 Нет изменений") + else: + raise + +# ====================================================================== +# CALLBACK HANDLERS +# ====================================================================== + +@router.callback_query(F.data == "settings:refresh") +async def refresh_settings(callback: CallbackQuery, state: FSMContext) -> None: + """Обновляет меню (с защитой от MessageNotModified)""" + await show_settings_menu(callback) + +@router.callback_query(F.data == "settings:close") +async def close_settings(callback: CallbackQuery, state: FSMContext) -> None: + """Закрывает меню""" + await state.clear() + try: + await callback.message.delete() + except: + pass + await callback.answer("❌ Закрыто") + +@router.callback_query(F.data == "settings:cancel") +async def cancel_edit(callback: CallbackQuery, state: FSMContext) -> None: + """Возврат в главное меню без сохранения""" + await state.clear() + await show_settings_menu(callback) + +@router.callback_query(F.data == "settings:admin_chat") +async def edit_admin_chat(callback: CallbackQuery, state: FSMContext) -> None: + """Редактирование админ-чата""" + await state.set_state(BotSettingsStates.waiting_admin_chat) + await callback.message.edit_text( + "📢 АДМИН-ЧАТ\n\n" + "Отправьте ID чата для уведомлений:\n" + "Пример: -1003764219200\n\n" + "Для отключения: null\n\n" + "Или нажмите кнопку ниже для возврата в меню.", + parse_mode="HTML", + reply_markup=cancel_keyboard() + ) + await callback.answer() + +@router.callback_query(F.data == "settings:admin_thread") +async def edit_admin_thread(callback: CallbackQuery, state: FSMContext) -> None: + """Редактирование топика админ-чата""" + await state.set_state(BotSettingsStates.waiting_admin_thread) + await callback.message.edit_text( + "🧵 ТОПИК АДМИН-ЧАТА\n\n" + "Отправьте ID топика:\n" + "Пример: 1\n\n" + "Для отключения: null\n\n" + "Или нажмите кнопку ниже для возврата в меню.", + parse_mode="HTML", + reply_markup=cancel_keyboard() + ) + await callback.answer() + +@router.callback_query(F.data == "settings:report_chat") +async def edit_report_chat(callback: CallbackQuery, state: FSMContext) -> None: + """Редактирование чата репортов""" + await state.set_state(BotSettingsStates.waiting_report_chat) + await callback.message.edit_text( + "📊 ЧАТ РЕПОРТОВ\n\n" + "Отправьте ID чата для репортов:\n" + "Пример: -1003764219200\n\n" + "Для отключения: null\n\n" + "Или нажмите кнопку ниже для возврата в меню.", + parse_mode="HTML", + reply_markup=cancel_keyboard() + ) + await callback.answer() + +@router.callback_query(F.data == "settings:report_thread") +async def edit_report_thread(callback: CallbackQuery, state: FSMContext) -> None: + """Редактирование топика репортов""" + await state.set_state(BotSettingsStates.waiting_report_thread) + await callback.message.edit_text( + "🧵 ТОПИК РЕПОРТОВ\n\n" + "Отправьте ID топика:\n" + "Пример: 1\n\n" + "Для отключения: null\n\n" + "Или нажмите кнопку ниже для возврата в меню.", + parse_mode="HTML", + reply_markup=cancel_keyboard() + ) + await callback.answer() + +# ====================================================================== +# MESSAGE HANDLERS (FSM) +# ====================================================================== + +@router.message(BotSettingsStates.waiting_admin_chat, IsAdmin()) +async def process_admin_chat(message: Message, state: FSMContext) -> None: + text = message.text.strip() + + if text == "/cancel": + await state.clear() + await message.answer("❌ Отменено") + return + + if text == "null": + value = None + else: + try: + value = int(text) + if not str(value).startswith('-'): + raise ValueError("ID чата должен начинаться с минуса") + except ValueError: + await message.answer("❌ Неверный формат. Пример: -1003764219200", parse_mode="HTML") + return + + manager = get_manager() + success = await manager.set_bot_setting("admin_chat_id", str(value) if value else None) + + await state.clear() + + if success: + # Показываем обновлённое главное меню + await show_settings_menu(message) + # Удаляем сообщение с вводом + try: + await message.delete() + except: + pass + else: + await message.answer("❌ Ошибка сохранения", parse_mode="HTML") + +@router.message(BotSettingsStates.waiting_admin_thread, IsAdmin()) +async def process_admin_thread(message: Message, state: FSMContext) -> None: + text = message.text.strip() + + if text == "/cancel": + await state.clear() + await message.answer("❌ Отменено") + return + + if text == "null": + value = None + else: + try: + value = int(text) + if value < 1: + raise ValueError("ID топика должен быть > 0") + except ValueError: + await message.answer("❌ Неверный формат. Пример: 1", parse_mode="HTML") + return + + manager = get_manager() + success = await manager.set_bot_setting("admin_thread_id", str(value) if value else None) + + await state.clear() + + if success: + await show_settings_menu(message) + try: + await message.delete() + except: + pass + else: + await message.answer("❌ Ошибка сохранения", parse_mode="HTML") + +@router.message(BotSettingsStates.waiting_report_chat, IsAdmin()) +async def process_report_chat(message: Message, state: FSMContext) -> None: + text = message.text.strip() + + if text == "/cancel": + await state.clear() + await message.answer("❌ Отменено") + return + + if text == "null": + value = None + else: + try: + value = int(text) + if not str(value).startswith('-'): + raise ValueError("ID чата должен начинаться с минуса") + except ValueError: + await message.answer("❌ Неверный формат. Пример: -1003764219200", parse_mode="HTML") + return + + manager = get_manager() + success = await manager.set_bot_setting("report_chat_id", str(value) if value else None) + + await state.clear() + + if success: + await show_settings_menu(message) + try: + await message.delete() + except: + pass + else: + await message.answer("❌ Ошибка сохранения", parse_mode="HTML") + +@router.message(BotSettingsStates.waiting_report_thread, IsAdmin()) +async def process_report_thread(message: Message, state: FSMContext) -> None: + text = message.text.strip() + + if text == "/cancel": + await state.clear() + await message.answer("❌ Отменено") + return + + if text == "null": + value = None + else: + try: + value = int(text) + if value < 1: + raise ValueError("ID топика должен быть > 0") + except ValueError: + await message.answer("❌ Неверный формат. Пример: 1", parse_mode="HTML") + return + + manager = get_manager() + success = await manager.set_bot_setting("report_thread_id", str(value) if value else None) + + await state.clear() + + if success: + await show_settings_menu(message) + try: + await message.delete() + except: + pass + else: + await message.answer("❌ Ошибка сохранения", parse_mode="HTML") + +@router.message(Command("cancel")) +async def cancel_settings(message: Message, state: FSMContext) -> None: + """Глобальный cancel""" + await state.clear() + await message.answer("✅ Настройки отменены")