Template
365 lines
13 KiB
Python
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
|