This commit is contained in:
2026-04-22 18:34:13 +03:00
parent 3992121397
commit ce7a1f70b2
60 changed files with 10243 additions and 3443 deletions

182
app/utils/formatters.py Normal file
View File

@@ -0,0 +1,182 @@
from __future__ import annotations
import html
import math
from datetime import datetime
from typing import Any
def format_bytes(value: int | float | None) -> str:
if value is None or value <= 0:
return "0 B"
units = ["B", "KB", "MB", "GB", "TB", "PB"]
size = float(value)
unit_index = 0
while size >= 1024 and unit_index < len(units) - 1:
size /= 1024
unit_index += 1
if unit_index == 0:
return f"{int(size)} {units[unit_index]}"
return f"{size:.2f} {units[unit_index]}"
def format_datetime(value: datetime | None) -> str:
if value is None:
return "не указано"
return value.strftime("%d.%m.%Y %H:%M")
def format_traffic_limit(limit_bytes: int | None) -> str:
if limit_bytes is None or limit_bytes <= 0:
return "без лимита"
return format_bytes(limit_bytes)
def _format_days_word(value: int) -> str:
remainder_100 = value % 100
remainder_10 = value % 10
if 11 <= remainder_100 <= 14:
return "дней"
if remainder_10 == 1:
return "день"
if 2 <= remainder_10 <= 4:
return "дня"
return "дней"
def format_datetime_with_days_left(value: datetime | None) -> str:
if value is None:
return "не указано"
now = datetime.now(value.tzinfo) if value.tzinfo is not None else datetime.now()
seconds_left = (value - now).total_seconds()
if seconds_left <= 0:
return f"{format_datetime(value)} (истёк)"
days_left = max(1, math.ceil(seconds_left / 86400))
return f"{format_datetime(value)} ({days_left} {_format_days_word(days_left)})"
def build_help_text(*, is_admin: bool) -> str:
lines = [
"<b>Команды</b>",
"/start - открыть главное меню",
"",
"Дальше используйте кнопки внутри интерфейса бота.",
]
return "\n".join(lines)
def format_payment_plan(
*,
title: str,
description: str,
price_stars: int,
duration_days: int,
traffic_limit_bytes: int,
) -> str:
lines = [
"<b>Тариф</b>",
"",
f"<b>{html.escape(title)}</b>",
html.escape(description),
"",
f"Срок: <b>{duration_days} дн.</b>",
f"Трафик: <b>{format_traffic_limit(traffic_limit_bytes)}</b>",
f"Цена: <b>{price_stars} ₽</b>",
]
return "\n".join(lines)
def format_issued_access(
*,
subscription_url: str,
expire_at: datetime,
traffic_limit_bytes: int,
brand_name: str = "",
is_renewal: bool = False,
) -> str:
normalized_brand_name = html.escape(brand_name.strip()) if brand_name.strip() else "VPN"
title = (
f"<b>🔥 {normalized_brand_name} — подписка продлена</b>"
if is_renewal
else f"<b>🔥 {normalized_brand_name} — доступ готов</b>"
)
status_text = (
"Подписка продлена. Откройте её кнопкой ниже "
"или импортируйте ссылку вручную в ваш VPN-клиент."
if is_renewal
else "Подписка активирована. Откройте её кнопкой ниже "
"или импортируйте ссылку вручную в ваш VPN-клиент."
)
lines = [
title,
"",
f"<blockquote>{status_text}</blockquote>",
"",
"<b>🔗 Ключ / подписка</b>",
html.escape(subscription_url),
"",
(
"<blockquote>"
f"⏳ Действует до: <b>{format_datetime_with_days_left(expire_at)}</b>\n"
f"📶 Трафик: <b>{format_traffic_limit(traffic_limit_bytes)}</b>"
"</blockquote>"
),
"",
"<b>📲 Что делать дальше</b>",
"1. Нажмите кнопку «Открыть подписку» ниже или откройте ссылку выше.",
"2. Импортируйте подписку или ключ в приложение VPN-клиента.",
"3. Управлять доступом и продлевать его можно в панели бота.",
"",
"<i>Если ссылка не открывается автоматически, скопируйте её в клиент вручную.</i>",
]
return "\n".join(lines)
def format_user_card(
*,
user: Any,
squad_names: list[str],
recent_requests: list[Any] | None = None,
) -> str:
lines = [
f"<b>{html.escape(user.username)}</b> <code>{html.escape(user.status)}</code>",
f"UUID: <code>{html.escape(user.rw_uuid)}</code>",
f"Short UUID: <code>{html.escape(user.short_uuid)}</code>",
f"Telegram ID: <code>{user.telegram_id if user.telegram_id is not None else '-'}</code>",
f"Истекает: <b>{format_datetime(user.expire_at)}</b>",
(
"Трафик: "
f"<b>{format_bytes(user.used_traffic_bytes)}</b> / "
f"<b>{format_traffic_limit(user.traffic_limit_bytes)}</b>"
),
f"За все время: <b>{format_bytes(user.lifetime_used_traffic_bytes)}</b>",
f"Последний UA: {html.escape(user.sub_last_user_agent or 'неизвестно')}",
f"Последнее открытие подписки: {format_datetime(user.sub_last_opened_at)}",
f"Подписка: {html.escape(user.subscription_url)}",
]
if user.email:
lines.append(f"Email: {html.escape(user.email)}")
if user.tag:
lines.append(f"Tag: <code>{html.escape(user.tag)}</code>")
if user.description:
lines.append(f"Описание: {html.escape(user.description)}")
if squad_names:
lines.append(f"Squads: {html.escape(', '.join(sorted(squad_names)))}")
if recent_requests:
lines.append("")
lines.append("<b>Последние запросы подписки</b>")
for request in recent_requests:
lines.append(
f"- {format_datetime(request.request_at)} | "
f"IP: {html.escape(request.request_ip or '-')} | "
f"UA: {html.escape((request.user_agent or '-')[:80])}"
)
return "\n".join(lines)