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("✅ Настройки отменены")