Initial commit: VPN Telegram bot (sanitized defaults)
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
"""Админ-панель."""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user