Files
RemnaWaveBOT/app/utils/formatters.py
2026-07-09 16:45:17 +03:00

199 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = "",
success_text: 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. Управлять доступом и продлевать его можно в панели бота.",
"",
(
"<blockquote>"
"💡 <b>О белых списках:</b> подписки с белыми списками предназначены "
"для использования в условиях полной блокировки. Если полной блокировки нет — "
"рекомендуем пользоваться VPN"
"</blockquote>"
),
"",
"<i>Если ссылка не открывается автоматически, скопируйте её в клиент вручную.</i>",
]
normalized_success_text = success_text.strip()
if normalized_success_text:
lines[7:7] = [
"<b>Информация</b>",
f"<blockquote>{html.escape(normalized_success_text)}</blockquote>",
"",
]
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)