"""Админ-панель.""" from __future__ import annotations from aiogram import F, Router from aiogram.filters import Command, CommandObject, StateFilter from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from remnawave import RemnawaveSDK from filters import IsAdmin from keyboards import ( admin_back_inline, admin_inline, admin_topup_amounts, admin_topup_cancel, ) from services.db import Database from services.nodes_status import fetch_nodes_status, format_nodes_status router = Router(name="admin") class TopUpStates(StatesGroup): waiting_user_id = State() waiting_custom_amount = State() def setup_admin_router(is_admin: IsAdmin) -> Router: router.message.filter(is_admin) router.callback_query.filter(is_admin) return router ADMIN_HOME = ( "🛠 Админ-панель\n\n" "Управление и мониторинг Remnawave.\n" "Пополнение: кнопка ниже или\n" "/addbal TELEGRAM_ID СУММА" ) @router.message(Command("admin")) @router.message(F.text == "🛠 Админка") async def admin_home(message: Message, state: FSMContext) -> None: await state.clear() await message.answer(ADMIN_HOME, reply_markup=admin_inline(), parse_mode="HTML") @router.callback_query(F.data == "admin:home") async def cb_admin_home(callback: CallbackQuery, state: FSMContext) -> None: await state.clear() await callback.answer() if callback.message: await callback.message.edit_text(ADMIN_HOME, reply_markup=admin_inline(), parse_mode="HTML") @router.callback_query(F.data == "admin:servers") async def cb_admin_servers(callback: CallbackQuery, sdk: RemnawaveSDK) -> None: await callback.answer("Пингую…") try: nodes = await fetch_nodes_status(sdk) text = format_nodes_status(nodes, detailed=True) if callback.message: await callback.message.edit_text( text, reply_markup=admin_back_inline(), parse_mode="HTML" ) except Exception as exc: # noqa: BLE001 if callback.message: await callback.message.edit_text( f"Ошибка:\n{exc}", reply_markup=admin_inline(), parse_mode="HTML", ) @router.callback_query(F.data == "admin:health") async def cb_admin_health(callback: CallbackQuery, sdk: RemnawaveSDK) -> None: await callback.answer() try: health = await sdk.system.get_health() try: meta = await sdk.system.get_metadata() version = getattr(meta, "version", "—") except Exception: # noqa: BLE001 version = "—" lines = [ "💚 Remnawave Health", "", f"Версия: {version}", "Статус API: OK", ] pm2 = getattr(health, "pm2_stats", None) or [] if pm2: lines.append("") lines.append("Процессы:") for proc in pm2: lines.append( f"• {proc.name} — CPU {proc.cpu}, RAM {proc.memory}" ) if callback.message: await callback.message.edit_text( "\n".join(lines), reply_markup=admin_inline(), parse_mode="HTML", ) except Exception as exc: # noqa: BLE001 if callback.message: await callback.message.edit_text( f"API недоступен.\n{exc}", reply_markup=admin_inline(), parse_mode="HTML", ) @router.callback_query(F.data == "admin:users") async def cb_admin_users(callback: CallbackQuery, sdk: RemnawaveSDK) -> None: await callback.answer() try: users = await sdk.users.get_all_users(size=1) total = getattr(users, "total", None) if total is None: items = getattr(users, "users", None) or getattr(users, "root", None) total = len(items) if items is not None else "?" text = f"👥 Пользователи\n\nВсего в панели: {total}" if callback.message: await callback.message.edit_text( text, reply_markup=admin_inline(), parse_mode="HTML" ) except Exception as exc: # noqa: BLE001 if callback.message: await callback.message.edit_text( f"Не удалось получить пользователей.\n{exc}", reply_markup=admin_inline(), parse_mode="HTML", ) @router.callback_query(F.data == "admin:stats") async def cb_admin_stats(callback: CallbackQuery, sdk: RemnawaveSDK) -> None: await callback.answer() try: stats = await sdk.system.get_stats() mem = stats.memory users = stats.users online = stats.online_stats nodes = stats.nodes text = ( "📊 Статистика панели\n\n" f"CPU cores: {stats.cpu.cores:g}\n" f"RAM: {mem.used / 1024**3:.1f} / {mem.total / 1024**3:.1f} GB\n\n" f"Пользователи: {users.total_users:g}\n" f"Онлайн сейчас: {online.online_now:g}\n" f"Онлайн за сутки: {online.last_day:g}\n\n" f"Ноды онлайн: {nodes.total_online:g}\n" f"Трафик lifetime: {nodes.total_bytes_lifetime}" ) if callback.message: await callback.message.edit_text( text, reply_markup=admin_inline(), parse_mode="HTML" ) except Exception as exc: # noqa: BLE001 if callback.message: await callback.message.edit_text( f"Статистика недоступна.\n{exc}", reply_markup=admin_inline(), parse_mode="HTML", ) @router.callback_query(F.data == "admin:topup") async def cb_admin_topup(callback: CallbackQuery, state: FSMContext) -> None: await state.set_state(TopUpStates.waiting_user_id) await callback.answer() if callback.message: await callback.message.edit_text( "💰 Пополнение баланса\n\n" "Пришли Telegram ID пользователя.\n" "Или используй: /addbal ID СУММА", reply_markup=admin_topup_cancel(), parse_mode="HTML", ) @router.callback_query(F.data == "admin:topup_cancel") async def cb_admin_topup_cancel(callback: CallbackQuery, state: FSMContext) -> None: await state.clear() await callback.answer("Отменено") if callback.message: await callback.message.edit_text( ADMIN_HOME, reply_markup=admin_inline(), parse_mode="HTML" ) @router.message(StateFilter(TopUpStates.waiting_user_id), F.text) async def admin_topup_got_id(message: Message, state: FSMContext, db: Database) -> None: text = (message.text or "").strip() if not text.isdigit(): await message.answer( "Нужен числовой Telegram ID.\nПример: 123456789", reply_markup=admin_topup_cancel(), parse_mode="HTML", ) return target_id = int(text) user = db.ensure_user(target_id) await state.clear() await message.answer( f"Пользователь {target_id}\n" f"Текущий баланс: {user.balance:.0f} ₽\n\n" "Выбери сумму пополнения:", reply_markup=admin_topup_amounts(target_id), parse_mode="HTML", ) @router.callback_query(F.data.startswith("admin:topup_custom:")) async def cb_admin_topup_custom(callback: CallbackQuery, state: FSMContext) -> None: target_id = int((callback.data or "").split(":")[-1]) await state.set_state(TopUpStates.waiting_custom_amount) await state.update_data(topup_target_id=target_id) await callback.answer() if callback.message: await callback.message.edit_text( f"Введи сумму пополнения для {target_id} (₽).\n" "Можно отрицательную — для списания.", reply_markup=admin_topup_cancel(), parse_mode="HTML", ) @router.message(StateFilter(TopUpStates.waiting_custom_amount), F.text) async def admin_topup_custom_amount( message: Message, state: FSMContext, db: Database, ) -> None: data = await state.get_data() target_id = int(data.get("topup_target_id") or 0) raw = (message.text or "").strip().replace(",", ".") try: amount = float(raw) except ValueError: await message.answer("Нужно число, например 750", parse_mode="HTML") return if amount == 0: await message.answer("Сумма не должна быть 0.") return await state.clear() admin = message.from_user balance = db.add_balance( target_id, amount, kind="admin_topup", meta=f"by:{admin.id if admin else '?'}", ) await message.answer( f"✅ Начислено {amount:.0f} ₽{target_id}\n" f"Баланс: {balance:.0f} ₽", reply_markup=admin_inline(), parse_mode="HTML", ) try: if message.bot: await message.bot.send_message( target_id, f"💰 Баланс изменён на {amount:+.0f} ₽\n" f"Доступно: {balance:.0f} ₽", parse_mode="HTML", ) except Exception: # noqa: BLE001 pass @router.callback_query(F.data.startswith("admin:topup_do:")) async def cb_admin_topup_do(callback: CallbackQuery, db: Database) -> None: parts = (callback.data or "").split(":") if len(parts) != 4: await callback.answer("Некорректные данные", show_alert=True) return try: target_id = int(parts[2]) amount = float(parts[3]) except ValueError: await callback.answer("Некорректные данные", show_alert=True) return admin = callback.from_user balance = db.add_balance( target_id, amount, kind="admin_topup", meta=f"by:{admin.id if admin else '?'}", ) await callback.answer("Готово") if callback.message: await callback.message.edit_text( f"✅ Начислено {amount:.0f} ₽\n" f"Пользователь: {target_id}\n" f"Новый баланс: {balance:.0f} ₽", reply_markup=admin_inline(), parse_mode="HTML", ) try: if callback.bot: await callback.bot.send_message( target_id, f"💰 Баланс пополнен на {amount:.0f} ₽\n" f"Доступно: {balance:.0f} ₽", parse_mode="HTML", ) except Exception: # noqa: BLE001 pass @router.message(Command("addbal")) async def cmd_addbal( message: Message, command: CommandObject, db: Database, ) -> None: args = (command.args or "").split() if len(args) != 2: await message.answer( "Формат: /addbal TELEGRAM_ID СУММА\n" "Пример: /addbal 123456789 500", parse_mode="HTML", ) return try: target_id = int(args[0]) amount = float(args[1]) except ValueError: await message.answer("ID и сумма должны быть числами.") return if amount == 0: await message.answer("Сумма не должна быть 0.") return admin = message.from_user balance = db.add_balance( target_id, amount, kind="admin_topup", meta=f"by:{admin.id if admin else '?'}", ) await message.answer( f"✅ Начислено {amount:.0f} ₽{target_id}\n" f"Баланс: {balance:.0f} ₽", parse_mode="HTML", ) try: if message.bot: await message.bot.send_message( target_id, f"💰 Баланс пополнен на {amount:.0f} ₽\n" f"Доступно: {balance:.0f} ₽", parse_mode="HTML", ) except Exception: # noqa: BLE001 pass