"""Пользовательские хендлеры."""
from __future__ import annotations
from aiogram import F, Router
from aiogram.filters import CommandObject, CommandStart
from aiogram.types import CallbackQuery, Message
from remnawave import RemnawaveSDK
from config import Settings
from keyboards import (
clients_links_inline,
clients_root_inline,
faq_back_inline,
faq_root_inline,
main_menu,
servers_inline,
)
from services.clients import CLIENTS_INTRO, client_card, get_client
from services.db import AuthError, Database
from services.faq import faq_answer_text, faq_intro, get_faq
from services.nodes_status import fetch_nodes_status, format_nodes_status
router = Router(name="user")
WELCOME = (
"👋 Привет, {name}!\n\n"
"Я бот VPN на Remnawave.\n"
"Открой Mini App или пользуйся кнопками ниже.\n\n"
"Баланс: {balance:.0f} ₽"
)
HELP = (
"ℹ️ Помощь\n\n"
"• Mini App — кабинет в Telegram\n"
"• Тарифы — оплата с баланса или криптой\n"
"• Баланс — Crypto Bot / Heleket / Lava / Digiseller / код\n"
"• Моя подписка — ссылка, срок, трафик, продление\n"
"• Скачать клиент — топ приложений\n"
"• Статус серверов — онлайн / оффлайн\n"
"• FAQ — частые вопросы и ответы\n"
"• Тикеты — написать в поддержку\n\n"
"Не нашёл ответ — создай тикет или открой FAQ."
)
def _is_admin(settings: Settings, user_id: int | None) -> bool:
return bool(user_id and user_id in settings.admin_ids)
def _kb(settings: Settings, is_admin: bool = False):
return main_menu(
is_admin=is_admin,
webapp_url=settings.webapp_url or None,
)
@router.message(CommandStart())
async def cmd_start(
message: Message,
command: CommandObject,
settings: Settings,
db: Database,
) -> None:
user = message.from_user
name = user.full_name if user else "друг"
telegram_id = user.id if user else 0
bot_user = db.ensure_user(
telegram_id,
username=user.username if user else None,
full_name=name if user else None,
)
admin = _is_admin(settings, telegram_id or None)
args = (command.args or "").strip()
if args.startswith("qrauth_") and telegram_id > 0:
token = args[len("qrauth_") :]
try:
db.approve_login_qr(token, telegram_id)
await message.answer(
"✅ Вход на сайт подтверждён\n\n"
"Можно вернуться в браузер — кабинет откроется автоматически.",
reply_markup=_kb(settings, admin),
parse_mode="HTML",
)
return
except AuthError as exc:
await message.answer(
f"⚠️ Не удалось подтвердить QR: {exc}\n\n"
+ WELCOME.format(name=name, balance=bot_user.balance),
reply_markup=_kb(settings, admin),
parse_mode="HTML",
)
return
if args.startswith("ref_") and telegram_id > 0:
code = args[len("ref_") :]
if db.attach_referrer(telegram_id, code):
await message.answer(
"🎁 Реферальная ссылка принята! Бонус начислим после первой покупки тарифа.",
parse_mode="HTML",
)
db.ensure_referral_code(telegram_id)
if bot_user.banned:
await message.answer(
"🚫 Аккаунт заблокирован. Напишите в поддержку, если это ошибка.",
parse_mode="HTML",
)
return
text = WELCOME.format(name=name, balance=bot_user.balance)
if not settings.webapp_url:
text += (
"\n\nMini App пока выключен: задай WEBAPP_URL (https) в .env"
)
await message.answer(
text,
reply_markup=_kb(settings, admin),
parse_mode="HTML",
)
@router.message(F.text == "ℹ️ Помощь")
async def cmd_help(message: Message, settings: Settings) -> None:
admin = _is_admin(settings, message.from_user.id if message.from_user else None)
await message.answer(HELP, reply_markup=_kb(settings, admin), parse_mode="HTML")
@router.message(F.text == "❓ FAQ")
async def cmd_faq(message: Message) -> None:
await message.answer(
faq_intro(),
reply_markup=faq_root_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data == "faq:root")
async def cb_faq_root(callback: CallbackQuery) -> None:
await callback.answer()
if callback.message:
await callback.message.edit_text(
faq_intro(),
reply_markup=faq_root_inline(),
parse_mode="HTML",
)
@router.callback_query(F.data.startswith("faq:view:"))
async def cb_faq_view(callback: CallbackQuery) -> None:
item_id = (callback.data or "").split(":", 2)[2]
item = get_faq(item_id)
if not item:
await callback.answer("Вопрос не найден", show_alert=True)
return
await callback.answer()
if callback.message:
await callback.message.edit_text(
faq_answer_text(item),
reply_markup=faq_back_inline(),
parse_mode="HTML",
)
@router.message(F.text == "📥 Скачать клиент")
async def cmd_clients(message: Message) -> None:
await message.answer(
CLIENTS_INTRO,
reply_markup=clients_root_inline(),
parse_mode="HTML",
disable_web_page_preview=True,
)
@router.callback_query(F.data == "clients:root")
async def cb_clients_root(callback: CallbackQuery) -> None:
await callback.answer()
if callback.message:
await callback.message.edit_text(
CLIENTS_INTRO,
reply_markup=clients_root_inline(),
parse_mode="HTML",
disable_web_page_preview=True,
)
@router.callback_query(F.data.startswith("clients:view:"))
async def cb_clients_view(callback: CallbackQuery) -> None:
client_id = (callback.data or "").split(":", 2)[2]
client = get_client(client_id)
if not client:
await callback.answer("Клиент не найден", show_alert=True)
return
await callback.answer()
if callback.message:
await callback.message.edit_text(
client_card(client),
reply_markup=clients_links_inline(client.links),
parse_mode="HTML",
disable_web_page_preview=True,
)
@router.message(F.text == "📡 Статус серверов")
async def cmd_servers(message: Message, sdk: RemnawaveSDK) -> None:
wait = await message.answer("⏳ Проверяю серверы…")
try:
nodes = await fetch_nodes_status(sdk)
text = format_nodes_status(nodes, detailed=False)
await wait.edit_text(text, reply_markup=servers_inline(), parse_mode="HTML")
except Exception as exc: # noqa: BLE001
await wait.edit_text(
f"Не удалось получить статус серверов.\n{exc}",
parse_mode="HTML",
)
@router.callback_query(F.data == "servers:refresh")
async def cb_servers_refresh(callback: CallbackQuery, sdk: RemnawaveSDK) -> None:
await callback.answer("Обновляю…")
try:
nodes = await fetch_nodes_status(sdk)
text = format_nodes_status(nodes, detailed=False)
if callback.message:
await callback.message.edit_text(
text, reply_markup=servers_inline(), parse_mode="HTML"
)
except Exception as exc: # noqa: BLE001
if callback.message:
await callback.message.edit_text(
f"Ошибка обновления.\n{exc}",
parse_mode="HTML",
)
@router.callback_query(F.data == "menu:home")
async def cb_menu_home(callback: CallbackQuery, settings: Settings) -> None:
await callback.answer()
admin = _is_admin(settings, callback.from_user.id if callback.from_user else None)
if callback.message:
await callback.message.answer(
"🏠 Главное меню",
reply_markup=_kb(settings, admin),
)