Initial commit: VPN Telegram bot (sanitized defaults)

This commit is contained in:
VPN Service
2026-07-26 23:21:41 +03:00
commit aaf04faa96
77 changed files with 33249 additions and 0 deletions
+717
View File
@@ -0,0 +1,717 @@
"""Клавиатуры бота."""
from __future__ import annotations
from aiogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
KeyboardButton,
ReplyKeyboardMarkup,
WebAppInfo,
)
from services.tariffs import Tariff
def main_menu(is_admin: bool = False, *, webapp_url: str | None = None) -> ReplyKeyboardMarkup:
rows: list[list[KeyboardButton]] = []
if webapp_url:
rows.append(
[
KeyboardButton(
text="🚀 Mini App",
web_app=WebAppInfo(url=webapp_url),
)
]
)
rows.extend(
[
[KeyboardButton(text="🛒 Тарифы"), KeyboardButton(text="💰 Баланс")],
[KeyboardButton(text="📱 Моя подписка"), KeyboardButton(text="📡 Статус серверов")],
[KeyboardButton(text="📥 Скачать клиент"), KeyboardButton(text="❓ FAQ")],
[KeyboardButton(text="🎫 Тикеты"), KeyboardButton(text="️ Помощь")],
]
)
if is_admin:
rows.append([KeyboardButton(text="🛠 Админка")])
return ReplyKeyboardMarkup(
keyboard=rows,
resize_keyboard=True,
input_field_placeholder="Выберите действие…",
)
def faq_root_inline() -> InlineKeyboardMarkup:
from services.faq import FAQ_ITEMS
rows: list[list[InlineKeyboardButton]] = []
for item in FAQ_ITEMS:
rows.append(
[
InlineKeyboardButton(
text=item.question,
callback_data=f"faq:view:{item.id}",
)
]
)
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def faq_back_inline() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="⬅️ Все вопросы", callback_data="faq:root")],
[InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")],
]
)
def clients_root_inline() -> InlineKeyboardMarkup:
from services.clients import TOP_CLIENTS
rows: list[list[InlineKeyboardButton]] = []
row: list[InlineKeyboardButton] = []
for client in TOP_CLIENTS:
prefix = "" if client.featured else ""
row.append(
InlineKeyboardButton(
text=f"{prefix}{client.name}",
callback_data=f"clients:view:{client.id}",
)
)
if len(row) == 2:
rows.append(row)
row = []
if row:
rows.append(row)
rows.append(
[InlineKeyboardButton(text="📚 Все клиенты (docs.rw)", url="https://docs.rw/clients")]
)
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def clients_links_inline(links: tuple, *, back: str = "clients:root") -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
for link in links:
rows.append([InlineKeyboardButton(text=f"⬇️ {link.title}", url=link.url)])
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=back)])
return InlineKeyboardMarkup(inline_keyboard=rows)
def servers_inline() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="servers:refresh")],
[InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")],
]
)
def balance_inline(
*,
crypto_enabled: bool = False,
heleket_enabled: bool = False,
digiseller_enabled: bool = False,
lava_enabled: bool = False,
anypay_enabled: bool = False,
stars_enabled: bool = False,
) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if stars_enabled:
rows.append(
[
InlineKeyboardButton(
text="⭐ Telegram Stars",
callback_data="pay:stars",
)
]
)
if crypto_enabled or heleket_enabled:
rows.append(
[InlineKeyboardButton(text="🪙 Пополнить криптой", callback_data="pay:topup")]
)
if lava_enabled:
rows.append(
[
InlineKeyboardButton(
text="💳 Lava (карта / СБП)",
callback_data="pay:lava",
)
]
)
if anypay_enabled:
rows.append(
[
InlineKeyboardButton(
text="💳 AnyPay",
callback_data="pay:anypay",
)
]
)
if digiseller_enabled:
rows.append(
[
InlineKeyboardButton(
text="💳 Digiseller",
callback_data="pay:digiseller",
)
]
)
rows.append(
[
InlineKeyboardButton(
text="🎫 Код пополнения",
callback_data="pay:topup_code",
)
]
)
rows.append([InlineKeyboardButton(text="🛒 Купить тариф", callback_data="shop:list")])
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def digiseller_inline(*, pay_url: str) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if pay_url:
rows.append([InlineKeyboardButton(text="💳 Оплатить на Digiseller", url=pay_url)])
rows.append(
[
InlineKeyboardButton(
text="🔄 Не зачислилось? Ввести код",
callback_data="pay:digiseller_code",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="pay:balance")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def digiseller_cancel_inline() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="pay:digiseller_cancel")]
]
)
def topup_code_inline(*, pay_url: str = "") -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if pay_url:
rows.append([InlineKeyboardButton(text="🛒 Купить код", url=pay_url)])
rows.append(
[InlineKeyboardButton(text="❌ Отмена", callback_data="pay:topup_code_cancel")]
)
return InlineKeyboardMarkup(inline_keyboard=rows)
def topup_providers_inline(
*,
crypto_enabled: bool,
heleket_enabled: bool,
) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if crypto_enabled:
rows.append(
[
InlineKeyboardButton(
text="Crypto Bot",
callback_data="pay:topup_provider:cryptobot",
)
]
)
if heleket_enabled:
rows.append(
[
InlineKeyboardButton(
text="Heleket (резерв)",
callback_data="pay:topup_provider:heleket",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="pay:balance")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def topup_amounts_inline(provider: str = "cryptobot") -> InlineKeyboardMarkup:
amounts = [50, 100, 200, 500, 1000, 2000, 3000]
rows: list[list[InlineKeyboardButton]] = []
row: list[InlineKeyboardButton] = []
for amount in amounts:
row.append(
InlineKeyboardButton(
text=f"{amount}",
callback_data=f"pay:topup_amount:{provider}:{amount}",
)
)
if len(row) == 3:
rows.append(row)
row = []
if row:
rows.append(row)
back = "pay:stars" if provider == "stars" else "pay:topup"
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=back)])
return InlineKeyboardMarkup(inline_keyboard=rows)
def stars_amounts_inline(*, rub_per_star: float) -> InlineKeyboardMarkup:
from services.stars import STAR_TOPUP_AMOUNTS_RUB, quote_topup
rows: list[list[InlineKeyboardButton]] = []
for amount in STAR_TOPUP_AMOUNTS_RUB:
q = quote_topup(amount, rub_per_star=rub_per_star)
rows.append(
[
InlineKeyboardButton(
text=f"{q.rub_label} · {q.stars_label}",
callback_data=f"pay:stars_amount:{amount}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="pay:balance")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def tariffs_inline(tariffs: list[Tariff]) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
for t in tariffs:
rows.append(
[
InlineKeyboardButton(
text=f"{t.title} · {t.format_price()}",
callback_data=f"shop:view:{t.id}",
)
]
)
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def renew_tariffs_inline(tariffs: list[Tariff]) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
for t in tariffs:
rows.append(
[
InlineKeyboardButton(
text=f" {t.title} · {t.format_price()}",
callback_data=f"renew:view:{t.id}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ К подписке", callback_data="sub:mine")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def renew_confirm_inline(tariff_id: str) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Продлить с баланса",
callback_data=f"renew:buy:{tariff_id}",
)
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="renew:list")],
]
)
def subscription_inline(
*,
has_subscription: bool,
devices_over: bool = False,
) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if has_subscription:
if devices_over:
rows.append(
[
InlineKeyboardButton(
text="⚠️ Оплатить лишние устройства",
callback_data="devices:list",
)
]
)
rows.append(
[
InlineKeyboardButton(
text="🔄 Продлить с баланса",
callback_data="renew:list",
)
]
)
rows.append(
[
InlineKeyboardButton(
text="📦 Докупить трафик",
callback_data="traffic:list",
)
]
)
rows.append(
[
InlineKeyboardButton(
text="📱 Доп. устройства",
callback_data="devices:list",
)
]
)
rows.append([InlineKeyboardButton(text="🛒 Тарифы", callback_data="shop:list")])
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def device_packs_inline(*, excess: int = 0) -> InlineKeyboardMarkup:
from services.devices import DEVICE_PACKS, EXTRA_DEVICE_PRICE_RUB
rows: list[list[InlineKeyboardButton]] = []
if excess > 0:
price = int(excess * EXTRA_DEVICE_PRICE_RUB)
rows.append(
[
InlineKeyboardButton(
text=f"⚠️ Оплатить превышение +{excess} · {price}",
callback_data=f"devices:confirm:{excess}",
)
]
)
for pack in DEVICE_PACKS:
rows.append(
[
InlineKeyboardButton(
text=f"{pack.title} · {pack.format_price()}",
callback_data=f"devices:buy:{pack.slots}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ К подписке", callback_data="sub:mine")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def device_confirm_inline(slots: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Купить с баланса",
callback_data=f"devices:confirm:{slots}",
)
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="devices:list")],
]
)
def traffic_packs_inline() -> InlineKeyboardMarkup:
from services.traffic import TRAFFIC_PACKS
rows: list[list[InlineKeyboardButton]] = []
for pack in TRAFFIC_PACKS:
rows.append(
[
InlineKeyboardButton(
text=f"{pack.title} · {pack.format_price()}",
callback_data=f"traffic:buy:{pack.packs}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ К подписке", callback_data="sub:mine")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def traffic_confirm_inline(packs: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Купить с баланса",
callback_data=f"traffic:confirm:{packs}",
)
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="traffic:list")],
]
)
def tariff_pay_methods_inline(
tariff_id: str,
*,
crypto_enabled: bool,
heleket_enabled: bool = False,
lava_enabled: bool = False,
anypay_enabled: bool = False,
) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = [
[
InlineKeyboardButton(
text="💰 С баланса",
callback_data=f"shop:buy_balance:{tariff_id}",
)
]
]
if crypto_enabled:
rows.append(
[
InlineKeyboardButton(
text="🪙 Crypto Bot",
callback_data=f"shop:buy_crypto:{tariff_id}",
)
]
)
if heleket_enabled:
rows.append(
[
InlineKeyboardButton(
text="🪙 Heleket",
callback_data=f"shop:buy_heleket:{tariff_id}",
)
]
)
if lava_enabled:
rows.append(
[
InlineKeyboardButton(
text="💳 Lava (карта / СБП)",
callback_data=f"shop:buy_lava:{tariff_id}",
)
]
)
if anypay_enabled:
rows.append(
[
InlineKeyboardButton(
text="💳 AnyPay",
callback_data=f"shop:buy_anypay:{tariff_id}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data="shop:list")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def invoice_inline(
invoice_id: int,
pay_url: str,
*,
provider: str = "cryptobot",
) -> InlineKeyboardMarkup:
if provider == "anypay":
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить AnyPay", url=pay_url)],
[InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")],
]
)
if provider == "heleket":
check_cb = f"pay:check_hk:{invoice_id}"
elif provider == "lava":
check_cb = f"pay:check_lava:{invoice_id}"
else:
check_cb = f"pay:check:{invoice_id}"
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="🪙 Оплатить", url=pay_url)],
[
InlineKeyboardButton(
text="✅ Я оплатил — проверить",
callback_data=check_cb,
)
],
[InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")],
]
)
def admin_inline() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="📡 Серверы", callback_data="admin:servers"),
InlineKeyboardButton(text="💚 Health API", callback_data="admin:health"),
],
[
InlineKeyboardButton(text="👥 Пользователи", callback_data="admin:users"),
InlineKeyboardButton(text="📊 Статистика", callback_data="admin:stats"),
],
[
InlineKeyboardButton(
text="🎫 Тикеты",
callback_data="admin:tickets",
)
],
[
InlineKeyboardButton(
text="💰 Пополнить пользователю",
callback_data="admin:topup",
)
],
[
InlineKeyboardButton(text="🔄 Обновить панель", callback_data="admin:home"),
InlineKeyboardButton(text="🏠 В меню", callback_data="menu:home"),
],
]
)
def tickets_root_inline(*, has_open: bool = False) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = [
[InlineKeyboardButton(text="✍️ Новый тикет", callback_data="tix:new")],
[InlineKeyboardButton(text="📋 Мои тикеты", callback_data="tix:list")],
]
if has_open:
rows.insert(
1,
[InlineKeyboardButton(text="🟢 Открытые", callback_data="tix:list:open")],
)
rows.append([InlineKeyboardButton(text="🏠 Меню", callback_data="menu:home")])
return InlineKeyboardMarkup(inline_keyboard=rows)
def tickets_list_inline(
tickets: list,
*,
prefix: str = "tix:view",
back: str = "tix:root",
) -> InlineKeyboardMarkup:
from services.tickets import status_label
rows: list[list[InlineKeyboardButton]] = []
for t in tickets[:20]:
tid = int(t["id"])
st = status_label(str(t.get("status") or ""), short=True)
subj = str(t.get("subject") or "")[:28]
rows.append(
[
InlineKeyboardButton(
text=f"#{tid} · {st} · {subj}",
callback_data=f"{prefix}:{tid}",
)
]
)
rows.append([InlineKeyboardButton(text="⬅️ Назад", callback_data=back)])
return InlineKeyboardMarkup(inline_keyboard=rows)
def ticket_view_inline(ticket_id: int, *, status: str, is_admin: bool = False) -> InlineKeyboardMarkup:
rows: list[list[InlineKeyboardButton]] = []
if status != "closed":
reply_cb = f"admin:tix:reply:{ticket_id}" if is_admin else f"tix:reply:{ticket_id}"
close_cb = f"admin:tix:close:{ticket_id}" if is_admin else f"tix:close:{ticket_id}"
rows.append([InlineKeyboardButton(text="💬 Ответить", callback_data=reply_cb)])
rows.append([InlineKeyboardButton(text="🔒 Закрыть", callback_data=close_cb)])
elif is_admin:
rows.append(
[
InlineKeyboardButton(
text="🔓 Открыть снова",
callback_data=f"admin:tix:reopen:{ticket_id}",
)
]
)
back = "admin:tickets" if is_admin else "tix:list"
rows.append([InlineKeyboardButton(text="⬅️ К списку", callback_data=back)])
return InlineKeyboardMarkup(inline_keyboard=rows)
def ticket_cancel_inline(*, back: str = "tix:root") -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data=back)],
]
)
def ticket_notify_inline(ticket_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Открыть тикет",
callback_data=f"tix:view:{ticket_id}",
)
]
]
)
def admin_ticket_notify_inline(ticket_id: int) -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Открыть тикет",
callback_data=f"admin:tix:view:{ticket_id}",
)
]
]
)
def admin_tickets_root_inline(stats: dict | None = None) -> InlineKeyboardMarkup:
open_n = int((stats or {}).get("open") or 0)
ans_n = int((stats or {}).get("answered") or 0)
rows = [
[
InlineKeyboardButton(
text=f"🟢 Открытые ({open_n})",
callback_data="admin:tickets:open",
)
],
[
InlineKeyboardButton(
text=f"💬 С ответом ({ans_n})",
callback_data="admin:tickets:answered",
)
],
[InlineKeyboardButton(text="📋 Все", callback_data="admin:tickets:all")],
[InlineKeyboardButton(text="⬅️ Админка", callback_data="admin:home")],
]
return InlineKeyboardMarkup(inline_keyboard=rows)
def admin_back_inline() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(text="🔄 Обновить", callback_data="admin:servers"),
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin:home"),
]
]
)
def admin_topup_cancel() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin:topup_cancel")]
]
)
def admin_topup_amounts(telegram_id: int) -> InlineKeyboardMarkup:
amounts = [100, 200, 500, 1000, 2000, 5000]
rows: list[list[InlineKeyboardButton]] = []
row: list[InlineKeyboardButton] = []
for amount in amounts:
row.append(
InlineKeyboardButton(
text=f"+{amount}",
callback_data=f"admin:topup_do:{telegram_id}:{amount}",
)
)
if len(row) == 3:
rows.append(row)
row = []
if row:
rows.append(row)
rows.append(
[
InlineKeyboardButton(
text="✏️ Своя сумма",
callback_data=f"admin:topup_custom:{telegram_id}",
)
]
)
rows.append([InlineKeyboardButton(text="❌ Отмена", callback_data="admin:topup_cancel")])
return InlineKeyboardMarkup(inline_keyboard=rows)