v2.0
This commit is contained in:
1
app/utils/__init__.py
Normal file
1
app/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Utility helpers."""
|
||||
182
app/utils/formatters.py
Normal file
182
app/utils/formatters.py
Normal 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)
|
||||
Reference in New Issue
Block a user