from aiogram import F, Router, types from aiogram.filters import Command, MagicData, StateFilter from aiogram.fsm.context import FSMContext from helper_bot.filters.main import ChatTypeFilter from helper_bot.handlers.admin.dependencies import AdminAccessMiddleware from helper_bot.handlers.admin.exceptions import ( InvalidInputError, UserAlreadyBannedError, ) from helper_bot.handlers.admin.services import AdminService from helper_bot.handlers.admin.utils import ( escape_html, format_ban_confirmation, format_user_info, handle_admin_error, return_to_admin_menu, ) from helper_bot.keyboards.keyboards import ( create_keyboard_for_approve_ban, create_keyboard_for_ban_days, create_keyboard_for_ban_reason, create_keyboard_with_pagination, get_auto_moderation_keyboard, get_reply_keyboard_admin, ) from helper_bot.utils.base_dependency_factory import get_global_instance # Local imports - metrics from helper_bot.utils.metrics import db_query_time, track_errors, track_time from logs.custom_logger import logger # Создаем роутер с middleware для проверки доступа admin_router = Router() admin_router.message.middleware(AdminAccessMiddleware()) # ============================================================================ # ХЕНДЛЕРЫ МЕНЮ # ============================================================================ @admin_router.message(ChatTypeFilter(chat_type=["private"]), Command("admin")) @track_time("admin_panel", "admin_handlers") @track_errors("admin_handlers", "admin_panel") async def admin_panel(message: types.Message, state: FSMContext, **kwargs): """Главное меню администратора""" try: await state.set_state("ADMIN") logger.info(f"Запуск админ панели для пользователя: {message.from_user.id}") markup = get_reply_keyboard_admin() await message.answer( "Добро пожаловать в админку. Выбери что хочешь:", reply_markup=markup ) except Exception as e: await handle_admin_error(message, e, state, "admin_panel") # ============================================================================ # ХЕНДЛЕР ОТМЕНЫ # ============================================================================ @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter( "AWAIT_BAN_TARGET", "AWAIT_BAN_DETAILS", "AWAIT_BAN_DURATION", "BAN_CONFIRMATION", ), F.text == "Отменить", ) @track_time("cancel_ban_process", "admin_handlers") @track_errors("admin_handlers", "cancel_ban_process") async def cancel_ban_process(message: types.Message, state: FSMContext, **kwargs): """Отмена процесса блокировки""" try: current_state = await state.get_state() logger.info(f"Отмена процедуры блокировки из состояния: {current_state}") await return_to_admin_menu(message, state) except Exception as e: await handle_admin_error(message, e, state, "cancel_ban_process") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("ADMIN"), F.text == "Бан (Список)", ) @track_time("get_last_users", "admin_handlers") @track_errors("admin_handlers", "get_last_users") @db_query_time("get_last_users", "users", "select") async def get_last_users( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Получение списка последних пользователей""" try: logger.info( f"Получение списка последних пользователей. Пользователь: {message.from_user.full_name}" ) admin_service = AdminService(bot_db) users = await admin_service.get_last_users() # Преобразуем в формат для клавиатуры (кортежи как ожидает create_keyboard_with_pagination) users_data = [(user.full_name, user.user_id) for user in users] keyboard = create_keyboard_with_pagination( 1, len(users_data), users_data, "ban" ) await message.answer( text="Список пользователей которые последними обращались к боту", reply_markup=keyboard, ) except Exception as e: await handle_admin_error(message, e, state, "get_last_users") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("ADMIN"), F.text == "Разбан (список)", ) @track_time("get_banned_users", "admin_handlers") @track_errors("admin_handlers", "get_banned_users") @db_query_time("get_banned_users", "users", "select") async def get_banned_users( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Получение списка заблокированных пользователей""" try: logger.info( f"Получение списка заблокированных пользователей. Пользователь: {message.from_user.full_name}" ) admin_service = AdminService(bot_db) message_text, buttons_list = await admin_service.get_banned_users_for_display(0) if buttons_list: keyboard = create_keyboard_with_pagination( 1, len(buttons_list), buttons_list, "unlock" ) await message.answer(text=message_text, reply_markup=keyboard, parse_mode="HTML") else: await message.answer( text="В списке заблокированных пользователей никого нет" ) except Exception as e: await handle_admin_error(message, e, state, "get_banned_users") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("ADMIN"), F.text == "📊 ML Статистика", ) @track_time("get_ml_stats", "admin_handlers") @track_errors("admin_handlers", "get_ml_stats") async def get_ml_stats(message: types.Message, state: FSMContext, **kwargs): """Получение статистики ML-скоринга""" try: logger.info( f"Запрос ML статистики от пользователя: {message.from_user.full_name}" ) bdf = get_global_instance() scoring_manager = bdf.get_scoring_manager() if not scoring_manager: await message.answer( "📊 ML Scoring отключен\n\nДля включения установите RAG_ENABLED=true или DEEPSEEK_ENABLED=true в .env" ) return stats = await scoring_manager.get_stats() # Формируем текст статистики lines = ["📊 ML Scoring Статистика\n"] # RAG статистика if "rag" in stats: rag = stats["rag"] lines.append("🤖 RAG API:") # Проверяем, есть ли данные из API (новый контракт содержит model_loaded и vector_store) if "model_loaded" in rag or "vector_store" in rag: # Данные из API /stats if "model_loaded" in rag: model_loaded = rag.get("model_loaded", False) lines.append( f" • Модель загружена: {'✅' if model_loaded else '❌'}" ) if "model_name" in rag: lines.append(f" • Модель: {rag.get('model_name', 'N/A')}") if "device" in rag: lines.append(f" • Устройство: {rag.get('device', 'N/A')}") # Статистика из vector_store if "vector_store" in rag: vector_store = rag["vector_store"] positive_count = vector_store.get("positive_count", 0) negative_count = vector_store.get("negative_count", 0) total_count = vector_store.get("total_count", 0) lines.append(f" • Положительных примеров: {positive_count}") lines.append(f" • Отрицательных примеров: {negative_count}") lines.append(f" • Всего примеров: {total_count}") if "vector_dim" in vector_store: lines.append( f" • Размерность векторов: {vector_store.get('vector_dim', 'N/A')}" ) if "max_examples" in vector_store: lines.append( f" • Макс. примеров: {vector_store.get('max_examples', 'N/A')}" ) else: # Fallback на синхронные данные (если API недоступен) lines.append(f" • API URL: {rag.get('api_url', 'N/A')}") if "enabled" in rag: if rag.get("enabled"): lines.append( f" • Статус: ⚠️ Включен, но API не отвечает" ) lines.append( f" • Проверьте доступность сервиса и API ключ" ) else: lines.append(f" • Статус: ❌ Отключен") lines.append("") # DeepSeek статистика if "deepseek" in stats: ds = stats["deepseek"] lines.append("🔮 DeepSeek API:") lines.append( f" • Статус: {'✅ Включен' if ds.get('enabled') else '❌ Отключен'}" ) lines.append(f" • Модель: {ds.get('model', 'N/A')}") lines.append(f" • Таймаут: {ds.get('timeout', 'N/A')}с") lines.append("") # Если ничего не включено if "rag" not in stats and "deepseek" not in stats: lines.append("⚠️ Ни один сервис не настроен") await message.answer("\n".join(lines), parse_mode="HTML") except Exception as e: logger.error(f"Ошибка получения ML статистики: {e}") await message.answer(f"❌ Ошибка получения статистики: {str(e)}") # ============================================================================ # ХЕНДЛЕРЫ АВТО-МОДЕРАЦИИ # ============================================================================ @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("ADMIN"), F.text == "⚙️ Авто-модерация", ) @track_time("auto_moderation_menu", "admin_handlers") @track_errors("admin_handlers", "auto_moderation_menu") async def auto_moderation_menu( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Меню управления авто-модерацией""" try: logger.info( f"Открытие меню авто-модерации пользователем: {message.from_user.full_name}" ) settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) await message.answer(text, reply_markup=keyboard, parse_mode="HTML") except Exception as e: logger.error(f"Ошибка открытия меню авто-модерации: {e}") await message.answer(f"❌ Ошибка: {str(e)}") def _format_auto_moderation_status(settings: dict) -> str: """Форматирует текст статуса авто-модерации.""" auto_publish = settings.get("auto_publish_enabled", False) auto_decline = settings.get("auto_decline_enabled", False) publish_threshold = settings.get("auto_publish_threshold", 0.8) decline_threshold = settings.get("auto_decline_threshold", 0.4) publish_status = "✅ Включена" if auto_publish else "❌ Выключена" decline_status = "✅ Включено" if auto_decline else "❌ Выключено" return ( "⚙️ Авто-модерация постов\n\n" f"🤖 Авто-публикация: {publish_status}\n" f" Порог: RAG score ≥ {publish_threshold}\n\n" f"🚫 Авто-отклонение: {decline_status}\n" f" Порог: RAG score ≤ {decline_threshold}" ) @admin_router.callback_query(F.data == "auto_mod_toggle_publish") @track_time("toggle_auto_publish", "admin_handlers") @track_errors("admin_handlers", "toggle_auto_publish") async def toggle_auto_publish(call: types.CallbackQuery, bot_db: MagicData("bot_db")): """Переключение авто-публикации""" try: new_state = await bot_db.toggle_auto_publish() logger.info( f"Авто-публикация {'включена' if new_state else 'выключена'} " f"пользователем {call.from_user.full_name}" ) settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) await call.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") await call.answer( f"Авто-публикация {'включена ✅' if new_state else 'выключена ❌'}" ) except Exception as e: logger.error(f"Ошибка переключения авто-публикации: {e}") await call.answer(f"❌ Ошибка: {str(e)}", show_alert=True) @admin_router.callback_query(F.data == "auto_mod_toggle_decline") @track_time("toggle_auto_decline", "admin_handlers") @track_errors("admin_handlers", "toggle_auto_decline") async def toggle_auto_decline(call: types.CallbackQuery, bot_db: MagicData("bot_db")): """Переключение авто-отклонения""" try: new_state = await bot_db.toggle_auto_decline() logger.info( f"Авто-отклонение {'включено' if new_state else 'выключено'} " f"пользователем {call.from_user.full_name}" ) settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) await call.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") await call.answer( f"Авто-отклонение {'включено ✅' if new_state else 'выключено ❌'}" ) except Exception as e: logger.error(f"Ошибка переключения авто-отклонения: {e}") await call.answer(f"❌ Ошибка: {str(e)}", show_alert=True) @admin_router.callback_query(F.data == "auto_mod_refresh") @track_time("refresh_auto_moderation", "admin_handlers") @track_errors("admin_handlers", "refresh_auto_moderation") async def refresh_auto_moderation( call: types.CallbackQuery, bot_db: MagicData("bot_db") ): """Обновление статуса авто-модерации""" try: settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) try: await call.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") except Exception as edit_error: if "message is not modified" in str(edit_error): pass # Сообщение не изменилось - это нормально else: raise await call.answer("🔄 Обновлено") except Exception as e: logger.error(f"Ошибка обновления статуса авто-модерации: {e}") await call.answer(f"❌ Ошибка: {str(e)}", show_alert=True) @admin_router.callback_query(F.data == "auto_mod_threshold_publish") @track_time("change_publish_threshold", "admin_handlers") @track_errors("admin_handlers", "change_publish_threshold") async def change_publish_threshold( call: types.CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db") ): """Начало изменения порога авто-публикации""" try: await state.set_state("AWAIT_PUBLISH_THRESHOLD") await call.message.answer( "📈 Изменение порога авто-публикации\n\n" "Введите новое значение порога (от 0.0 до 1.0).\n" "Посты с RAG score ≥ этого значения будут автоматически публиковаться.\n\n" "Текущее рекомендуемое значение: 0.8", parse_mode="HTML", ) await call.answer() except Exception as e: logger.error(f"Ошибка начала изменения порога публикации: {e}") await call.answer(f"❌ Ошибка: {str(e)}", show_alert=True) @admin_router.callback_query(F.data == "auto_mod_threshold_decline") @track_time("change_decline_threshold", "admin_handlers") @track_errors("admin_handlers", "change_decline_threshold") async def change_decline_threshold( call: types.CallbackQuery, state: FSMContext, bot_db: MagicData("bot_db") ): """Начало изменения порога авто-отклонения""" try: await state.set_state("AWAIT_DECLINE_THRESHOLD") await call.message.answer( "📉 Изменение порога авто-отклонения\n\n" "Введите новое значение порога (от 0.0 до 1.0).\n" "Посты с RAG score ≤ этого значения будут автоматически отклоняться.\n\n" "Текущее рекомендуемое значение: 0.4", parse_mode="HTML", ) await call.answer() except Exception as e: logger.error(f"Ошибка начала изменения порога отклонения: {e}") await call.answer(f"❌ Ошибка: {str(e)}", show_alert=True) @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_PUBLISH_THRESHOLD"), ) @track_time("process_publish_threshold", "admin_handlers") @track_errors("admin_handlers", "process_publish_threshold") async def process_publish_threshold( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Обработка нового порога авто-публикации""" try: value = float(message.text.strip().replace(",", ".")) if not 0.0 <= value <= 1.0: raise ValueError("Значение должно быть от 0.0 до 1.0") await bot_db.set_float_setting("auto_publish_threshold", value) logger.info( f"Порог авто-публикации изменен на {value} " f"пользователем {message.from_user.full_name}" ) await state.set_state("ADMIN") await message.answer( f"✅ Порог авто-публикации изменен на {value}", parse_mode="HTML", ) settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) await message.answer(text, reply_markup=keyboard, parse_mode="HTML") except ValueError as e: await message.answer( f"❌ Неверное значение: {e}\n" "Введите число от 0.0 до 1.0 (например: 0.8)" ) except Exception as e: logger.error(f"Ошибка изменения порога публикации: {e}") await state.set_state("ADMIN") await message.answer(f"❌ Ошибка: {str(e)}") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_DECLINE_THRESHOLD"), ) @track_time("process_decline_threshold", "admin_handlers") @track_errors("admin_handlers", "process_decline_threshold") async def process_decline_threshold( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Обработка нового порога авто-отклонения""" try: value = float(message.text.strip().replace(",", ".")) if not 0.0 <= value <= 1.0: raise ValueError("Значение должно быть от 0.0 до 1.0") await bot_db.set_float_setting("auto_decline_threshold", value) logger.info( f"Порог авто-отклонения изменен на {value} " f"пользователем {message.from_user.full_name}" ) await state.set_state("ADMIN") await message.answer( f"✅ Порог авто-отклонения изменен на {value}", parse_mode="HTML", ) settings = await bot_db.get_auto_moderation_settings() text = _format_auto_moderation_status(settings) keyboard = get_auto_moderation_keyboard(settings) await message.answer(text, reply_markup=keyboard, parse_mode="HTML") except ValueError as e: await message.answer( f"❌ Неверное значение: {e}\n" "Введите число от 0.0 до 1.0 (например: 0.4)" ) except Exception as e: logger.error(f"Ошибка изменения порога отклонения: {e}") await state.set_state("ADMIN") await message.answer(f"❌ Ошибка: {str(e)}") # ============================================================================ # ХЕНДЛЕРЫ ПРОЦЕССА БАНА # ============================================================================ @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("ADMIN"), F.text.in_(["Бан по нику", "Бан по ID"]), ) @track_time("start_ban_process", "admin_handlers") @track_errors("admin_handlers", "start_ban_process") async def start_ban_process(message: types.Message, state: FSMContext, **kwargs): """Начало процесса блокировки пользователя""" try: ban_type = "username" if message.text == "Бан по нику" else "id" await state.update_data(ban_type=ban_type) prompt_text = ( "Пришли мне username блокируемого пользователя" if ban_type == "username" else "Пришли мне ID блокируемого пользователя" ) await message.answer(prompt_text) await state.set_state("AWAIT_BAN_TARGET") except Exception as e: await handle_admin_error(message, e, state, "start_ban_process") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_TARGET") ) @track_time("process_ban_target", "admin_handlers") @track_errors("admin_handlers", "process_ban_target") async def process_ban_target( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db") ): """Обработка введенного username/ID для блокировки""" logger.info( f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}" ) try: user_data = await state.get_data() ban_type = user_data.get("ban_type") admin_service = AdminService(bot_db) logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}") # Определяем пользователя if ban_type == "username": logger.info( f"process_ban_target: Поиск пользователя по username: {message.text}" ) user = await admin_service.get_user_by_username(message.text) if not user: logger.warning( f"process_ban_target: Пользователь с username '{message.text}' не найден" ) await message.answer( f"Пользователь с username '{escape_html(message.text)}' не найден." ) await return_to_admin_menu(message, state) return else: # ban_type == "id" try: logger.info( f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}" ) user_id = await admin_service.validate_user_input(message.text) user = await admin_service.get_user_by_id(user_id) if not user: logger.warning( f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных" ) await message.answer( f"Пользователь с ID {user_id} не найден в базе данных." ) await return_to_admin_menu(message, state) return except InvalidInputError as e: logger.error(f"process_ban_target: Ошибка валидации ID: {e}") await message.answer(str(e)) await return_to_admin_menu(message, state) return logger.info( f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}" ) # Сохраняем данные пользователя await state.update_data( target_user_id=user.user_id, target_username=user.username, target_full_name=user.full_name, ) # Показываем информацию о пользователе и запрашиваем причину user_info = format_user_info(user.user_id, user.username, user.full_name) markup = create_keyboard_for_ban_reason() logger.info( f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}" ) await message.answer( text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат", reply_markup=markup, ) await state.set_state("AWAIT_BAN_DETAILS") logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS") except Exception as e: logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True) await handle_admin_error(message, e, state, "process_ban_target") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DETAILS") ) @track_time("process_ban_reason", "admin_handlers") @track_errors("admin_handlers", "process_ban_reason") async def process_ban_reason(message: types.Message, state: FSMContext, **kwargs): """Обработка причины блокировки""" logger.info( f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}" ) try: # Проверяем текущее состояние current_state = await state.get_state() logger.info(f"process_ban_reason: Текущее состояние: {current_state}") # Проверяем данные состояния state_data = await state.get_data() logger.info(f"process_ban_reason: Данные состояния: {state_data}") logger.info( f"process_ban_reason: Обновление данных состояния с причиной: {message.text}" ) await state.update_data(ban_reason=message.text) markup = create_keyboard_for_ban_days() safe_reason = escape_html(message.text) logger.info( f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}" ) await message.answer( f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат", reply_markup=markup, ) await state.set_state("AWAIT_BAN_DURATION") logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION") except Exception as e: logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True) await handle_admin_error(message, e, state, "process_ban_reason") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("AWAIT_BAN_DURATION") ) @track_time("process_ban_duration", "admin_handlers") @track_errors("admin_handlers", "process_ban_duration") async def process_ban_duration(message: types.Message, state: FSMContext, **kwargs): """Обработка срока блокировки""" try: user_data = await state.get_data() # Определяем срок блокировки if message.text == "Навсегда": ban_days = None else: try: ban_days = int(message.text) if ban_days <= 0: await message.answer( "Срок блокировки должен быть положительным числом." ) return except ValueError: await message.answer( "Пожалуйста, введите корректное число дней или выберите 'Навсегда'." ) return await state.update_data(ban_days=ban_days) # Показываем подтверждение confirmation_text = format_ban_confirmation( user_data["target_user_id"], user_data["ban_reason"], ban_days ) markup = create_keyboard_for_approve_ban() await message.answer(confirmation_text, reply_markup=markup) await state.set_state("BAN_CONFIRMATION") except Exception as e: await handle_admin_error(message, e, state, "process_ban_duration") @admin_router.message( ChatTypeFilter(chat_type=["private"]), StateFilter("BAN_CONFIRMATION"), F.text == "Подтвердить", ) @track_time("confirm_ban", "admin_handlers") @track_errors("admin_handlers", "confirm_ban") async def confirm_ban( message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), **kwargs ): """Подтверждение блокировки пользователя""" try: user_data = await state.get_data() admin_service = AdminService(bot_db) # Выполняем блокировку await admin_service.ban_user( user_id=user_data["target_user_id"], username=user_data["target_username"], reason=user_data["ban_reason"], ban_days=user_data["ban_days"], ban_author_id=message.from_user.id, ) safe_username = escape_html(user_data["target_username"]) await message.reply(f"Пользователь {safe_username} успешно заблокирован.") await return_to_admin_menu(message, state) except UserAlreadyBannedError as e: await message.reply(str(e)) await return_to_admin_menu(message, state) except Exception as e: await handle_admin_error(message, e, state, "confirm_ban")