9de34900a2
Bot uses default HTML parse mode — all messages are HTML-parsed by Telegram. Added html.escape() to all user-controlled and admin-controlled strings before interpolation into HTML messages to prevent injection and parse errors. 49 files, ~250+ injection points fixed: - user.full_name, first_name across all handlers and services - tariff.name/description in purchase flow, admin panel, auto-purchase service - campaign.name, start_parameter in admin and user-facing handlers - group.name, promo_group.name across promo management - contest.title, prize_text, leaderboard names (including public channels) - transaction.description (contains raw user.full_name from referral service) - restriction_reason across all balance and subscription handlers - ticket.title, message_text, poll.title, poll.description - welcome text template placeholders (first_name, username) - maintenance reason, admin_name, selected_prize.display_name New helpers in app/utils/formatting.py: - safe_html_name() for escaping display names - user_html_link() replacing 15+ duplicated inline link patterns
50 lines
1.4 KiB
Python
50 lines
1.4 KiB
Python
"""Shared formatting utilities for traffic, price, and period display."""
|
|
|
|
import html
|
|
|
|
|
|
def safe_html_name(name: str | None) -> str:
|
|
"""HTML-escape a display name for Telegram HTML messages."""
|
|
return html.escape(name or '')
|
|
|
|
|
|
def user_html_link(user) -> str:
|
|
"""Build an HTML-safe clickable user link for Telegram messages."""
|
|
safe = safe_html_name(user.full_name)
|
|
if getattr(user, 'telegram_id', None):
|
|
return f'<a href="tg://user?id={user.telegram_id}">{safe}</a>'
|
|
return f'<b>{safe}</b>'
|
|
|
|
|
|
def format_traffic(gb: int) -> str:
|
|
"""Форматирует трафик."""
|
|
if gb == 0:
|
|
return 'Безлимит'
|
|
return f'{gb} ГБ'
|
|
|
|
|
|
def format_price_kopeks(kopeks: int, compact: bool = False) -> str:
|
|
"""Форматирует цену из копеек в рубли."""
|
|
rubles = kopeks / 100
|
|
if compact:
|
|
# Компактный формат - округляем до рублей
|
|
return f'{int(round(rubles))}₽'
|
|
if rubles == int(rubles):
|
|
return f'{int(rubles)} ₽'
|
|
return f'{rubles:.2f} ₽'
|
|
|
|
|
|
def format_period(days: int) -> str:
|
|
"""Форматирует период."""
|
|
mod100 = days % 100
|
|
mod10 = days % 10
|
|
if 11 <= mod100 <= 19:
|
|
word = 'дней'
|
|
elif mod10 == 1:
|
|
word = 'день'
|
|
elif 2 <= mod10 <= 4:
|
|
word = 'дня'
|
|
else:
|
|
word = 'дней'
|
|
return f'{days} {word}'
|