Files

365 lines
13 KiB
Python

"""Админ-панель."""
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 = (
"<b>🛠 Админ-панель</b>\n\n"
"Управление и мониторинг Remnawave.\n"
"Пополнение: кнопка ниже или\n"
"<code>/addbal TELEGRAM_ID СУММА</code>"
)
@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<code>{exc}</code>",
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 = [
"<b>💚 Remnawave Health</b>",
"",
f"Версия: <code>{version}</code>",
"Статус API: <b>OK</b>",
]
pm2 = getattr(health, "pm2_stats", None) or []
if pm2:
lines.append("")
lines.append("<b>Процессы:</b>")
for proc in pm2:
lines.append(
f"• <code>{proc.name}</code> — 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<code>{exc}</code>",
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"<b>👥 Пользователи</b>\n\nВсего в панели: <b>{total}</b>"
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<code>{exc}</code>",
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 = (
"<b>📊 Статистика панели</b>\n\n"
f"CPU cores: <b>{stats.cpu.cores:g}</b>\n"
f"RAM: <b>{mem.used / 1024**3:.1f}</b> / {mem.total / 1024**3:.1f} GB\n\n"
f"Пользователи: <b>{users.total_users:g}</b>\n"
f"Онлайн сейчас: <b>{online.online_now:g}</b>\n"
f"Онлайн за сутки: <b>{online.last_day:g}</b>\n\n"
f"Ноды онлайн: <b>{nodes.total_online:g}</b>\n"
f"Трафик lifetime: <code>{nodes.total_bytes_lifetime}</code>"
)
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<code>{exc}</code>",
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(
"<b>💰 Пополнение баланса</b>\n\n"
"Пришли <b>Telegram ID</b> пользователя.\n"
"Или используй: <code>/addbal ID СУММА</code>",
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Пример: <code>123456789</code>",
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"Пользователь <code>{target_id}</code>\n"
f"Текущий баланс: <b>{user.balance:.0f} ₽</b>\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"Введи сумму пополнения для <code>{target_id}</code> (₽).\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("Нужно число, например <code>750</code>", 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"✅ Начислено <b>{amount:.0f} ₽</b> → <code>{target_id}</code>\n"
f"Баланс: <b>{balance:.0f} ₽</b>",
reply_markup=admin_inline(),
parse_mode="HTML",
)
try:
if message.bot:
await message.bot.send_message(
target_id,
f"💰 Баланс изменён на <b>{amount:+.0f} ₽</b>\n"
f"Доступно: <b>{balance:.0f} ₽</b>",
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"✅ Начислено <b>{amount:.0f} ₽</b>\n"
f"Пользователь: <code>{target_id}</code>\n"
f"Новый баланс: <b>{balance:.0f} ₽</b>",
reply_markup=admin_inline(),
parse_mode="HTML",
)
try:
if callback.bot:
await callback.bot.send_message(
target_id,
f"💰 Баланс пополнен на <b>{amount:.0f} ₽</b>\n"
f"Доступно: <b>{balance:.0f} ₽</b>",
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(
"Формат: <code>/addbal TELEGRAM_ID СУММА</code>\n"
"Пример: <code>/addbal 123456789 500</code>",
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"✅ Начислено <b>{amount:.0f} ₽</b> → <code>{target_id}</code>\n"
f"Баланс: <b>{balance:.0f} ₽</b>",
parse_mode="HTML",
)
try:
if message.bot:
await message.bot.send_message(
target_id,
f"💰 Баланс пополнен на <b>{amount:.0f} ₽</b>\n"
f"Доступно: <b>{balance:.0f} ₽</b>",
parse_mode="HTML",
)
except Exception: # noqa: BLE001
pass