fix: comprehensive html.escape() for all user/admin data in Telegram HTML messages

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
This commit is contained in:
Fringg
2026-03-23 08:06:15 +03:00
parent aec04f0085
commit 9de34900a2
49 changed files with 449 additions and 393 deletions
+5 -4
View File
@@ -1,3 +1,4 @@
import html
from datetime import UTC, datetime
import structlog
@@ -221,10 +222,10 @@ def replace_placeholders(text: str, user) -> str:
first_name = first_name.strip() if first_name else None
username = username.strip() if username else None
user_name = first_name or username or 'друг'
display_first_name = first_name or 'друг'
display_username = f'@{username}' if username else (first_name or 'друг')
clean_username = username or first_name or 'друг'
user_name = html.escape(first_name or username or 'друг')
display_first_name = html.escape(first_name or 'друг')
display_username = f'@{html.escape(username)}' if username else html.escape(first_name or 'друг')
clean_username = html.escape(username or first_name or 'друг')
replacements = {
'{user_name}': user_name,
+3 -1
View File
@@ -2,6 +2,8 @@
Обработчики админ-панели для управления черным списком
"""
import html
import structlog
from aiogram import types
from aiogram.filters import StateFilter
@@ -147,7 +149,7 @@ async def show_blacklist_users(callback: types.CallbackQuery, db_user: User, sta
# Показываем первые 20 записей
for i, (tg_id, username, reason) in enumerate(blacklist_users[:20], 1):
text += f'{i}. <code>{tg_id}</code> {username or ""}{reason}\n'
text += f'{i}. <code>{tg_id}</code> {html.escape(username or "")}{html.escape(reason or "")}\n'
if len(blacklist_users) > 20:
text += f'\n... и еще {len(blacklist_users) - 20} записей'
+2 -1
View File
@@ -5,6 +5,7 @@
и выполнять очистку БД и панели Remnawave.
"""
import html
from datetime import UTC, datetime
from enum import Enum
from typing import Any
@@ -437,7 +438,7 @@ async def show_blocked_list(
name = user_data.get('full_name') or user_data.get('username') or 'Без имени'
telegram_id = user_data.get('telegram_id', '?')
text += BlockedUsersText.BLOCKED_USER_ROW.value.format(
name=name,
name=html.escape(name),
telegram_id=telegram_id,
)
+8 -7
View File
@@ -1,3 +1,4 @@
import html
import re
import structlog
@@ -67,8 +68,8 @@ def _format_campaign_summary(campaign, texts) -> str:
bonus_info = '❓ Неизвестный тип бонуса'
return (
f'<b>{campaign.name}</b>\n'
f'Стартовый параметр: <code>{campaign.start_parameter}</code>\n'
f'<b>{html.escape(campaign.name)}</b>\n'
f'Стартовый параметр: <code>{html.escape(campaign.start_parameter)}</code>\n'
f'Статус: {status}\n'
f'{bonus_info}\n'
)
@@ -244,7 +245,7 @@ async def show_campaigns_list(
total_balance = sum(r.balance_bonus_kopeks or 0 for r in regs)
status = '🟢' if campaign.is_active else ''
line = (
f'{status} <b>{campaign.name}</b> — <code>{campaign.start_parameter}</code>\n'
f'{status} <b>{html.escape(campaign.name)}</b> — <code>{html.escape(campaign.start_parameter)}</code>\n'
f' Регистраций: {registrations}, баланс: {texts.format_price(total_balance)}'
)
if campaign.is_subscription_bonus:
@@ -383,7 +384,7 @@ async def start_edit_campaign_name(
await callback.message.edit_text(
(
'✏️ <b>Изменение названия кампании</b>\n\n'
f'Текущее название: <b>{campaign.name}</b>\n'
f'Текущее название: <b>{html.escape(campaign.name)}</b>\n'
'Введите новое название (3-100 символов):'
),
reply_markup=types.InlineKeyboardMarkup(
@@ -1183,8 +1184,8 @@ async def confirm_delete_campaign(
text = (
'🗑️ <b>Удаление кампании</b>\n\n'
f'Название: <b>{campaign.name}</b>\n'
f'Параметр: <code>{campaign.start_parameter}</code>\n\n'
f'Название: <b>{html.escape(campaign.name)}</b>\n'
f'Параметр: <code>{html.escape(campaign.start_parameter)}</code>\n\n'
'Вы уверены, что хотите удалить кампанию?'
)
@@ -1591,7 +1592,7 @@ async def select_campaign_tariff(
await state.update_data(campaign_tariff_id=tariff_id, campaign_tariff_name=tariff.name)
await state.set_state(AdminStates.creating_campaign_tariff_days)
await callback.message.edit_text(
f'🎁 Выбран тариф: <b>{tariff.name}</b>\n\n📅 Введите длительность тарифа в днях (1-730):',
f'🎁 Выбран тариф: <b>{html.escape(tariff.name)}</b>\n\n📅 Введите длительность тарифа в днях (1-730):',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_campaigns')]]
),
+16 -15
View File
@@ -1,3 +1,4 @@
import html
import math
from datetime import UTC, datetime, time
from zoneinfo import ZoneInfo
@@ -70,7 +71,7 @@ def _format_contest_summary(contest, texts, tz: ZoneInfo) -> str:
f'Дневная сводка: <b>{summary_times}</b>',
]
if contest.prize_text:
parts.append(texts.t('ADMIN_CONTEST_PRIZE', 'Приз: {prize}').format(prize=contest.prize_text))
parts.append(texts.t('ADMIN_CONTEST_PRIZE', 'Приз: {prize}').format(prize=html.escape(contest.prize_text)))
if contest.last_daily_summary_date:
parts.append(
texts.t('ADMIN_CONTEST_LAST_DAILY', 'Последняя сводка: {date}').format(
@@ -188,7 +189,7 @@ async def list_contests(
lines.append(texts.t('ADMIN_CONTESTS_EMPTY', 'Пока нет созданных конкурсов.'))
else:
for contest in contests:
lines.append(f'• <b>{contest.title}</b> (#{contest.id})')
lines.append(f'• <b>{html.escape(contest.title)}</b> (#{contest.id})')
contest_tz = _ensure_timezone(contest.timezone or settings.TIMEZONE)
lines.append(_format_contest_summary(contest, texts, contest_tz))
lines.append('')
@@ -250,21 +251,21 @@ async def show_contest_details(
total_events = await get_contest_events_count(db, contest.id) + virtual_count
lines = [
f'🏆 <b>{contest.title}</b>',
f'🏆 <b>{html.escape(contest.title)}</b>',
_format_contest_summary(contest, texts, tz),
texts.t('ADMIN_CONTEST_TOTAL_EVENTS', 'Зачётов: <b>{count}</b>').format(count=total_events),
]
if contest.description:
lines.append('')
lines.append(contest.description)
lines.append(html.escape(contest.description))
if leaderboard:
lines.append('')
lines.append(texts.t('ADMIN_CONTEST_LEADERBOARD_TITLE', '📊 Топ участников:'))
for idx, (name, score, _, is_virtual) in enumerate(leaderboard, start=1):
virt_mark = ' 👻' if is_virtual else ''
lines.append(f'{idx}. {name}{virt_mark}{score}')
lines.append(f'{idx}. {html.escape(name)}{virt_mark}{score}')
await callback.message.edit_text(
'\n'.join(lines),
@@ -444,7 +445,7 @@ async def show_leaderboard(
]
for idx, (name, score, _, is_virtual) in enumerate(leaderboard, start=1):
virt_mark = ' 👻' if is_virtual else ''
lines.append(f'{idx}. {name}{virt_mark}{score}')
lines.append(f'{idx}. {html.escape(name)}{virt_mark}{score}')
await callback.message.edit_text(
'\n'.join(lines),
@@ -690,7 +691,7 @@ async def show_detailed_stats(
# Общее сообщение с основной статистикой
general_lines = [
'📈 <b>Статистика конкурса</b>',
f'🏆 {contest.title}',
f'🏆 {html.escape(contest.title)}',
'',
f'👥 Участников (рефереров): <b>{stats["total_participants"]}</b>',
f'📨 Приглашено рефералов: <b>{stats["total_invited"]}</b>',
@@ -751,7 +752,7 @@ async def show_detailed_stats_page(
for p in page_participants:
lines.extend(
[
f'• <b>{p["full_name"]}</b>',
f'• <b>{html.escape(p["full_name"] or "")}</b>',
f' 📨 Приглашено: {p["total_referrals"]}',
f' 💰 Оплатили: {p["paid_referrals"]}',
f' ❌ Не оплатили: {p["unpaid_referrals"]}',
@@ -828,7 +829,7 @@ async def sync_contest(
lines = [
'✅ <b>Синхронизация завершена!</b>',
'',
f'📊 <b>Конкурс:</b> {contest.title}',
f'📊 <b>Конкурс:</b> {html.escape(contest.title)}',
f'📅 <b>Период:</b> {contest.start_at.strftime("%d.%m.%Y")} - {contest.end_at.strftime("%d.%m.%Y")}',
'🔍 <b>Фильтр транзакций:</b>',
f' <code>{start_str}</code>',
@@ -870,7 +871,7 @@ async def sync_contest(
# Обновляем основное сообщение с новой статистикой
detailed_stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id)
general_lines = [
f'🏆 <b>{contest.title}</b>',
f'🏆 <b>{html.escape(contest.title)}</b>',
f'📅 Период: {contest.start_at.strftime("%d.%m.%Y")} - {contest.end_at.strftime("%d.%m.%Y")}',
'',
f'👥 Участников (рефереров): <b>{detailed_stats["total_participants"]}</b>',
@@ -927,7 +928,7 @@ async def debug_contest_transactions(
lines = [
'🔍 <b>Отладка транзакций конкурса</b>',
'',
f'📊 <b>Конкурс:</b> {contest.title}',
f'📊 <b>Конкурс:</b> {html.escape(contest.title)}',
'📅 <b>Период фильтрации:</b>',
f' Начало: <code>{debug_data.get("contest_start")}</code>',
f' Конец: <code>{debug_data.get("contest_end")}</code>',
@@ -1002,10 +1003,10 @@ async def show_virtual_participants(
vps = await list_virtual_participants(db, contest_id)
lines = [f'👻 <b>Виртуальные участники</b> — {contest.title}', '']
lines = [f'👻 <b>Виртуальные участники</b> — {html.escape(contest.title)}', '']
if vps:
for vp in vps:
lines.append(f'{vp.display_name}{vp.referral_count} реф.')
lines.append(f'{html.escape(vp.display_name)}{vp.referral_count} реф.')
else:
lines.append('Пока нет виртуальных участников.')
@@ -1156,10 +1157,10 @@ async def delete_virtual_participant_handler(
vps = await list_virtual_participants(db, contest_id)
contest = await get_referral_contest(db, contest_id)
lines = [f'👻 <b>Виртуальные участники</b> — {contest.title}', '']
lines = [f'👻 <b>Виртуальные участники</b> — {html.escape(contest.title)}', '']
if vps:
for v in vps:
lines.append(f'{v.display_name}{v.referral_count} реф.')
lines.append(f'{html.escape(v.display_name)}{v.referral_count} реф.')
else:
lines.append('Пока нет виртуальных участников.')
+3 -1
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import Dispatcher, F, types
from aiogram.filters import Command
@@ -283,7 +285,7 @@ async def clear_rules_command(message: types.Message, db_user: User, db: AsyncSe
f'📊 <b>Статистика:</b>\n'
f'• Очищено правил: {stats["total_active"]}\n'
f'• Язык: {db_user.language}\n'
f'• Выполнил: {db_user.full_name}\n\n'
f'• Выполнил: {html.escape(db_user.full_name or "")}\n\n'
f'Теперь используются стандартные правила по умолчанию.'
)
+3 -1
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import Dispatcher, F, types
from aiogram.fsm.context import FSMContext
@@ -133,7 +135,7 @@ async def process_maintenance_reason(message: types.Message, db_user: User, db:
if success:
response_text = 'Режим техработ включен'
if reason:
response_text += f'\nПричина: {reason}'
response_text += f'\nПричина: {html.escape(reason)}'
else:
response_text = 'Ошибка включения режима техработ'
+2 -2
View File
@@ -643,7 +643,7 @@ async def show_messages_history(callback: types.CallbackQuery, db_user: User, db
{status_emoji} <b>{broadcast.created_at.strftime('%d.%m.%Y %H:%M')}</b>
📊 Отправлено: {broadcast.sent_count}/{broadcast.total_count} ({success_rate}%)
🎯 Аудитория: {get_target_name(broadcast.target_type)}
👤 Админ: {broadcast.admin_name}
👤 Админ: {html.escape(broadcast.admin_name or '')}
📝 Сообщение: {message_preview}
"""
@@ -1477,7 +1477,7 @@ async def confirm_broadcast(callback: types.CallbackQuery, db_user: User, state:
f'• Не доставлено: {failed_count}\n'
f'• Всего пользователей: {total_users_count}\n'
f'• Успешность: {success_rate}%{media_info}\n\n'
f'<b>Администратор:</b> {admin_name}'
f'<b>Администратор:</b> {html.escape(admin_name)}'
)
back_keyboard = types.InlineKeyboardMarkup(
+2 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import html
from datetime import UTC, date, datetime, timedelta
import structlog
@@ -741,7 +742,7 @@ async def traffic_check_callback(callback: CallbackQuery):
if violations:
text += '\n⚠️ <b>Превышения дельты:</b>\n'
for v in violations[:10]:
name = v.full_name or v.user_uuid[:8]
name = html.escape(v.full_name or '') or v.user_uuid[:8]
text += f'{name}: +{v.used_traffic_gb:.1f} ГБ\n'
if len(violations) > 10:
text += f'... и ещё {len(violations) - 10}\n'
+1 -1
View File
@@ -886,7 +886,7 @@ async def _render_poll_details(poll: Poll, language: str) -> str:
texts = get_texts(language)
lines = [f'🗳️ <b>{html.escape(poll.title)}</b>']
if poll.description:
lines.append(poll.description)
lines.append(html.escape(poll.description))
lines.append(_format_reward_text(poll, language))
lines.append(texts.t('ADMIN_POLLS_QUESTIONS_COUNT', 'Вопросов: {count}').format(count=len(poll.questions)))
+20 -18
View File
@@ -1,3 +1,4 @@
import html
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
import structlog
@@ -293,7 +294,7 @@ def _build_edit_menu_content(
header = texts.t(
'ADMIN_PROMO_GROUP_EDIT_MENU_TITLE',
'✏️ Настройки промогруппы «{name}»',
).format(name=group.name)
).format(name=html.escape(group.name))
lines = [header]
lines.extend(_format_discount_lines(texts, group))
@@ -468,7 +469,7 @@ async def show_promo_groups_menu(
'ADMIN_PROMO_GROUPS_MEMBERS_COUNT',
'Участников: {count}',
).format(count=member_count)
lines.append(f'{icon} <b>{group.name}</b>{default_suffix}{members_label}')
lines.append(f'{icon} <b>{html.escape(group.name)}</b>{default_suffix}{members_label}')
keyboard_rows.append(
[
types.InlineKeyboardButton(
@@ -524,7 +525,7 @@ async def show_promo_group_details(
texts.t(
'ADMIN_PROMO_GROUP_DETAILS_TITLE',
'💳 <b>Промогруппа:</b> {name}',
).format(name=group.name)
).format(name=html.escape(group.name))
]
lines.extend(_format_discount_lines(texts, group))
lines.append(_format_auto_assign_line(texts, group))
@@ -802,7 +803,7 @@ async def process_create_group_auto_assign(
await state.clear()
await message.answer(
texts.t('ADMIN_PROMO_GROUP_CREATED', 'Промогруппа «{name}» создана.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_CREATED', 'Промогруппа «{name}» создана.').format(name=html.escape(group.name)),
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -875,7 +876,7 @@ async def prompt_edit_promo_group_field(
prompt = texts.t(
'ADMIN_PROMO_GROUP_EDIT_NAME_PROMPT',
'Введите новое название промогруппы (текущее: {name}):',
).format(name=group.name)
).format(name=html.escape(group.name))
elif field == 'priority':
await state.set_state(AdminStates.editing_promo_group_priority)
prompt = texts.t(
@@ -951,7 +952,7 @@ async def process_edit_group_name(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -993,7 +994,7 @@ async def process_edit_group_priority(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1028,7 +1029,7 @@ async def process_edit_group_traffic(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1063,7 +1064,7 @@ async def process_edit_group_servers(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1098,7 +1099,7 @@ async def process_edit_group_devices(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1138,7 +1139,7 @@ async def process_edit_group_period_discounts(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1182,7 +1183,7 @@ async def process_edit_group_auto_assign(
texts,
group,
data.get('language', db_user.language),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_UPDATED', 'Промогруппа «{name}» обновлена.').format(name=html.escape(group.name)),
)
@@ -1212,19 +1213,20 @@ async def show_promo_group_members(
title = texts.t(
'ADMIN_PROMO_GROUP_MEMBERS_TITLE',
'👥 Участники группы {name}',
).format(name=group.name)
).format(name=html.escape(group.name))
if not members:
body = texts.t('ADMIN_PROMO_GROUP_MEMBERS_EMPTY', 'В этой группе пока нет участников.')
else:
lines = []
for index, user in enumerate(members, start=offset + 1):
username = f'@{user.username}' if user.username else ''
username = f'@{html.escape(user.username)}' if user.username else ''
safe_name = html.escape(user.full_name or '')
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
user_link = f'<a href="tg://user?id={user.telegram_id}">{safe_name}</a>'
tg_display = str(user.telegram_id)
else:
user_link = f'<b>{user.full_name}</b>'
user_link = f'<b>{safe_name}</b>'
tg_display = user.email or f'#{user.id}'
lines.append(f'{index}. {user_link} (ID {user.id}, {username}, TG {tg_display})')
body = '\n'.join(lines)
@@ -1273,7 +1275,7 @@ async def request_delete_promo_group(
confirm_text = texts.t(
'ADMIN_PROMO_GROUP_DELETE_CONFIRM',
'Удалить промогруппу «{name}»? Все пользователи будут переведены в базовую группу.',
).format(name=group.name)
).format(name=html.escape(group.name))
await callback.message.edit_text(
confirm_text,
@@ -1308,7 +1310,7 @@ async def delete_promo_group_confirmed(
return
await callback.message.edit_text(
texts.t('ADMIN_PROMO_GROUP_DELETED', 'Промогруппа «{name}» удалена.').format(name=group.name),
texts.t('ADMIN_PROMO_GROUP_DELETED', 'Промогруппа «{name}» удалена.').format(name=html.escape(group.name)),
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data='admin_promo_groups')]]
),
+1 -1
View File
@@ -677,7 +677,7 @@ def _describe_offer(
label = texts.t(config.get('label_key', ''), config.get('default_label', template.offer_type))
icon = config.get('icon', '📨')
lines = [f'{icon} <b>{template.name}</b>', '']
lines = [f'{icon} <b>{html.escape(template.name)}</b>', '']
lines.append(texts.t('ADMIN_PROMO_OFFER_TYPE', 'Тип: {label}').format(label=label))
lines.append(texts.t('ADMIN_PROMO_OFFER_VALID', 'Срок действия: {hours} ч').format(hours=template.valid_hours))
+9 -6
View File
@@ -1,3 +1,4 @@
import html
from datetime import UTC, datetime, timedelta
import structlog
@@ -94,7 +95,7 @@ async def show_promocodes_list(callback: types.CallbackQuery, db_user: User, db:
text += f'📅 Дней: {promo.subscription_days}\n'
elif promo.type == PromoCodeType.PROMO_GROUP.value:
if promo.promo_group:
text += f'🏷️ Промогруппа: {promo.promo_group.name}\n'
text += f'🏷️ Промогруппа: {html.escape(promo.promo_group.name)}\n'
elif promo.type == PromoCodeType.DISCOUNT.value:
discount_hours = promo.subscription_days
if discount_hours > 0:
@@ -170,7 +171,7 @@ async def show_promocode_management(callback: types.CallbackQuery, db_user: User
text += f'📅 <b>Дней:</b> {promo.subscription_days}\n'
elif promo.type == PromoCodeType.PROMO_GROUP.value:
if promo.promo_group:
text += f'🏷️ <b>Промогруппа:</b> {promo.promo_group.name} (приоритет: {promo.promo_group.priority})\n'
text += f'🏷️ <b>Промогруппа:</b> {html.escape(promo.promo_group.name)} (приоритет: {promo.promo_group.priority})\n'
elif promo.promo_group_id:
text += f'🏷️ <b>Промогруппа ID:</b> {promo.promo_group_id} (не найдена)\n'
elif promo.type == PromoCodeType.DISCOUNT.value:
@@ -472,7 +473,9 @@ async def process_promocode_code(message: types.Message, db_user: User, state: F
text = f'🏷️ <b>Промокод:</b> <code>{code}</code>\n\nВыберите промогруппу для назначения:\n\n'
for promo_group, user_count in groups_with_counts:
text += f'{promo_group.name} (приоритет: {promo_group.priority}, пользователей: {user_count})\n'
text += (
f'{html.escape(promo_group.name)} (приоритет: {promo_group.priority}, пользователей: {user_count})\n'
)
keyboard.append(
[
types.InlineKeyboardButton(
@@ -509,7 +512,7 @@ async def process_promo_group_selection(
await callback.message.edit_text(
f'🏷️ <b>Промокод для промогруппы</b>\n\n'
f'Промогруппа: {promo_group.name}\n'
f'Промогруппа: {html.escape(promo_group.name)}\n'
f'Приоритет: {promo_group.priority}\n\n'
f'📊 Введите количество использований промокода (или 0 для безлимита):'
)
@@ -1039,9 +1042,9 @@ async def show_promocode_stats(callback: types.CallbackQuery, db_user: User, db:
use_date = format_datetime(use.used_at)
if hasattr(use, 'user_username') and use.user_username:
user_display = f'@{use.user_username}'
user_display = f'@{html.escape(use.user_username)}'
elif hasattr(use, 'user_full_name') and use.user_full_name:
user_display = use.user_full_name
user_display = html.escape(use.user_full_name)
elif hasattr(use, 'user_telegram_id'):
user_display = f'ID{use.user_telegram_id}'
else:
+64 -34
View File
@@ -1,4 +1,5 @@
import asyncio
import html
import json
from datetime import UTC, datetime, timedelta
@@ -218,9 +219,9 @@ async def _show_top_referrers_filtered(callback: types.CallbackQuery, db: AsyncS
id_display = telegram_id or user_email or f'#{user_id}' if user_id else 'N/A'
if username:
display_text = f'@{username} (ID{id_display})'
display_text = f'@{html.escape(username)} (ID{id_display})'
elif display_name and display_name != f'ID{id_display}':
display_text = f'{display_name} (ID{id_display})'
display_text = f'{html.escape(display_name)} (ID{id_display})'
else:
display_text = f'ID{id_display}'
@@ -312,7 +313,7 @@ async def show_pending_withdrawal_requests(callback: types.CallbackQuery, db_use
for req in requests[:10]:
user = await get_user_by_id(db, req.user_id)
user_name = user.full_name if user else 'Неизвестно'
user_name = html.escape(user.full_name) if user and user.full_name else 'Неизвестно'
user_tg_id = user.telegram_id if user else 'N/A'
risk_emoji = (
@@ -359,7 +360,7 @@ async def view_withdrawal_request(callback: types.CallbackQuery, db_user: User,
return
user = await get_user_by_id(db, request.user_id)
user_name = user.full_name if user else 'Неизвестно'
user_name = html.escape(user.full_name) if user and user.full_name else 'Неизвестно'
user_tg_id = (user.telegram_id or user.email or f'#{user.id}') if user else 'N/A'
analysis = json.loads(request.risk_analysis) if request.risk_analysis else {}
@@ -381,7 +382,7 @@ async def view_withdrawal_request(callback: types.CallbackQuery, db_user: User,
📊 Статус: {status_text}
💳 <b>Реквизиты:</b>
<code>{request.payment_details}</code>
<code>{html.escape(request.payment_details or '')}</code>
📅 Создана: {request.created_at.strftime('%d.%m.%Y %H:%M')}
@@ -639,7 +640,7 @@ async def process_test_referral_earning(message: types.Message, db_user: User, d
await message.answer(
f'✅ <b>Тестовое начисление создано!</b>\n\n'
f'👤 Пользователь: {target_user.full_name or "Без имени"}\n'
f'👤 Пользователь: {html.escape(target_user.full_name) if target_user.full_name else "Без имени"}\n'
f'🆔 ID: <code>{target_telegram_id}</code>\n'
f'💰 Сумма: <b>{amount_rubles:.0f}₽</b>\n'
f'💳 Новый баланс: <b>{target_user.balance_kopeks / 100:.0f}₽</b>\n\n'
@@ -736,14 +737,17 @@ async def _show_diagnostics_for_period(callback: types.CallbackQuery, db: AsyncS
status = f'⚡ Другой реферер (ID{lost.current_referrer_id})'
# Имя или ID
user_name = lost.username or lost.full_name or f'ID{lost.telegram_id}'
if lost.username:
user_name = f'@{lost.username}'
user_name = f'@{html.escape(lost.username)}'
elif lost.full_name:
user_name = html.escape(lost.full_name)
else:
user_name = f'ID{lost.telegram_id}'
# Ожидаемый реферер
referrer_info = ''
if lost.expected_referrer_name:
referrer_info = f'{lost.expected_referrer_name}'
referrer_info = f'{html.escape(lost.expected_referrer_name)}'
elif lost.expected_referrer_id:
referrer_info = f' → ID{lost.expected_referrer_id}'
@@ -751,7 +755,7 @@ async def _show_diagnostics_for_period(callback: types.CallbackQuery, db: AsyncS
time_str = lost.click_time.strftime('%H:%M')
text += f'{i}. {user_name}{status}\n'
text += f' <code>{lost.referral_code}</code>{referrer_info} ({time_str})\n'
text += f' <code>{html.escape(lost.referral_code)}</code>{referrer_info} ({time_str})\n'
if len(report.lost_referrals) > 15:
text += f'\n<i>... и ещё {len(report.lost_referrals) - 15}</i>\n'
@@ -872,16 +876,22 @@ async def preview_referral_fixes(callback: types.CallbackQuery, db_user: User, d
# Показываем первые 10 деталей
for i, detail in enumerate(fix_report.details[:10], 1):
user_name = detail.username or detail.full_name or f'ID{detail.telegram_id}'
if detail.username:
user_name = f'@{detail.username}'
user_name = f'@{html.escape(detail.username)}'
elif detail.full_name:
user_name = html.escape(detail.full_name)
else:
user_name = f'ID{detail.telegram_id}'
if detail.error:
text += f'{i}. {user_name} — ❌ {detail.error}\n'
text += f'{i}. {user_name} — ❌ {html.escape(str(detail.error))}\n'
else:
text += f'{i}. {user_name}\n'
if detail.referred_by_set:
text += f' • Реферер: {detail.referrer_name or f"ID{detail.referrer_id}"}\n'
referrer_display = (
html.escape(detail.referrer_name) if detail.referrer_name else f'ID{detail.referrer_id}'
)
text += f' • Реферер: {referrer_display}\n'
if detail.had_first_topup:
text += f' • Первое пополнение: {settings.format_price(detail.topup_amount_kopeks)}\n'
if detail.bonus_to_referral_kopeks > 0:
@@ -967,13 +977,19 @@ async def apply_referral_fixes(callback: types.CallbackQuery, db_user: User, db:
for detail in fix_report.details:
if not detail.error and success_count < 10:
success_count += 1
user_name = detail.username or detail.full_name or f'ID{detail.telegram_id}'
if detail.username:
user_name = f'@{user_name}'
user_name = f'@{html.escape(detail.username)}'
elif detail.full_name:
user_name = html.escape(detail.full_name)
else:
user_name = f'ID{detail.telegram_id}'
text += f'{success_count}. {user_name}\n'
if detail.referred_by_set:
text += f' • Реферер: {detail.referrer_name or f"ID{detail.referrer_id}"}\n'
referrer_display = (
html.escape(detail.referrer_name) if detail.referrer_name else f'ID{detail.referrer_id}'
)
text += f' • Реферер: {referrer_display}\n'
if detail.bonus_to_referral_kopeks > 0:
text += f' • Бонус рефералу: {settings.format_price(detail.bonus_to_referral_kopeks)}\n'
if detail.bonus_to_referrer_kopeks > 0:
@@ -989,8 +1005,13 @@ async def apply_referral_fixes(callback: types.CallbackQuery, db_user: User, db:
for detail in fix_report.details:
if detail.error and error_count < 5:
error_count += 1
user_name = detail.username or detail.full_name or f'ID{detail.telegram_id}'
text += f'{user_name}: {detail.error}\n'
if detail.username:
user_name = f'@{html.escape(detail.username)}'
elif detail.full_name:
user_name = html.escape(detail.full_name)
else:
user_name = f'ID{detail.telegram_id}'
text += f'{user_name}: {html.escape(str(detail.error))}\n'
if fix_report.errors > 5:
text += f'<i>... и ещё {fix_report.errors - 5} ошибок</i>\n'
@@ -1055,8 +1076,12 @@ async def check_missing_bonuses(callback: types.CallbackQuery, db_user: User, db
👤 <b>Список ({len(report.missing_bonuses)} чел.):</b>
"""
for i, mb in enumerate(report.missing_bonuses[:15], 1):
referral_name = mb.referral_full_name or mb.referral_username or str(mb.referral_telegram_id)
referrer_name = mb.referrer_full_name or mb.referrer_username or str(mb.referrer_telegram_id)
referral_name = html.escape(
mb.referral_full_name or mb.referral_username or str(mb.referral_telegram_id)
)
referrer_name = html.escape(
mb.referrer_full_name or mb.referrer_username or str(mb.referrer_telegram_id)
)
text += f'\n{i}. <b>{referral_name}</b>'
text += f'\n └ Пригласил: {referrer_name}'
text += f'\n └ Пополнение: {mb.first_topup_amount_kopeks / 100:.0f}'
@@ -1191,9 +1216,9 @@ async def sync_referrals_with_contest(
total_created += stats.get('created', 0)
total_updated += stats.get('updated', 0)
total_skipped += stats.get('skipped', 0)
contest_results.append(f'{contest.title}: +{stats.get("created", 0)} новых')
contest_results.append(f'{html.escape(contest.title)}: +{stats.get("created", 0)} новых')
else:
contest_results.append(f'{contest.title}: ошибка')
contest_results.append(f'{html.escape(contest.title)}: ошибка')
text = f"""
🏆 <b>Синхронизация с конкурсами завершена!</b>
@@ -1275,7 +1300,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
if file_ext not in ['.log', '.txt']:
await message.answer(
f'❌ Неверный формат файла: {file_ext}\n\nПоддерживаются только текстовые файлы (.log, .txt)',
f'❌ Неверный формат файла: {html.escape(file_ext)}\n\nПоддерживаются только текстовые файлы (.log, .txt)',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='❌ Отмена', callback_data='admin_referral_diagnostics')]
@@ -1299,7 +1324,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
# Информируем о начале загрузки
status_message = await message.answer(
f'📥 Загружаю файл {file_name} ({message.document.file_size / 1024 / 1024:.1f} MB)...'
f'📥 Загружаю файл {html.escape(file_name)} ({message.document.file_size / 1024 / 1024:.1f} MB)...'
)
temp_file_path = None
@@ -1316,7 +1341,9 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
logger.info('📥 Файл загружен: ( байт)', temp_file_path=temp_file_path, file_size=message.document.file_size)
# Обновляем статус
await status_message.edit_text(f'🔍 Анализирую файл {file_name}...\n\nЭто может занять некоторое время.')
await status_message.edit_text(
f'🔍 Анализирую файл {html.escape(file_name)}...\n\nЭто может занять некоторое время.'
)
# Анализируем файл
from app.services.referral_diagnostics_service import referral_diagnostics_service
@@ -1325,7 +1352,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
# Формируем отчёт
text = f"""
🔍 <b>Анализ лог-файла: {file_name}</b>
🔍 <b>Анализ лог-файла: {html.escape(file_name)}</b>
<b>📊 Статистика переходов:</b>
Всего кликов по реф-ссылкам: {report.total_ref_clicks}
@@ -1348,14 +1375,17 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
status = f'⚡ Другой реферер (ID{lost.current_referrer_id})'
# Имя или ID
user_name = lost.username or lost.full_name or f'ID{lost.telegram_id}'
if lost.username:
user_name = f'@{lost.username}'
user_name = f'@{html.escape(lost.username)}'
elif lost.full_name:
user_name = html.escape(lost.full_name)
else:
user_name = f'ID{lost.telegram_id}'
# Ожидаемый реферер
referrer_info = ''
if lost.expected_referrer_name:
referrer_info = f'{lost.expected_referrer_name}'
referrer_info = f'{html.escape(lost.expected_referrer_name)}'
elif lost.expected_referrer_id:
referrer_info = f' → ID{lost.expected_referrer_id}'
@@ -1363,7 +1393,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
time_str = lost.click_time.strftime('%d.%m.%Y %H:%M')
text += f'{i}. {user_name}{status}\n'
text += f' <code>{lost.referral_code}</code>{referrer_info} ({time_str})\n'
text += f' <code>{html.escape(lost.referral_code)}</code>{referrer_info} ({time_str})\n'
if len(report.lost_referrals) > 15:
text += f'\n<i>... и ещё {len(report.lost_referrals) - 15}</i>\n'
@@ -1408,8 +1438,8 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
try:
await status_message.edit_text(
f'❌ <b>Ошибка при анализе файла</b>\n\n'
f'Файл: {file_name}\n'
f'Ошибка: {e!s}\n\n'
f'Файл: {html.escape(file_name)}\n'
f'Ошибка: {html.escape(str(e))}\n\n'
f'Проверьте, что файл является текстовым логом бота.',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
@@ -1428,7 +1458,7 @@ async def receive_log_file(message: types.Message, db_user: User, db: AsyncSessi
)
except:
await message.answer(
f'❌ Ошибка при анализе файла: {e!s}',
f'❌ Ошибка при анализе файла: {html.escape(str(e))}',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_referral_diagnostics')]
+22 -20
View File
@@ -1,5 +1,7 @@
"""Управление тарифами в админ-панели."""
import html
import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
@@ -317,7 +319,7 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st
price_block = f'<b>Цены:</b>\n{prices_display}'
tariff_type = '📅 Периодный'
return f"""📦 <b>Тариф: {tariff.name}</b>
return f"""📦 <b>Тариф: {html.escape(tariff.name)}</b>
{status} | {tariff_type}
🎚 Уровень: {tariff.tier_level}
@@ -343,7 +345,7 @@ def format_tariff_info(tariff: Tariff, language: str, subs_count: int = 0) -> st
📊 Подписок на тарифе: {subs_count}
{f'📝 {tariff.description}' if tariff.description else ''}"""
{f'📝 {html.escape(tariff.description)}' if tariff.description else ''}"""
@admin_required
@@ -591,7 +593,7 @@ async def start_edit_daily_price(
await callback.message.edit_text(
f'💰 <b>Редактирование суточной цены</b>\n\n'
f'Тариф: {tariff.name}\n'
f'Тариф: {html.escape(tariff.name)}\n'
f'Текущая цена: {format_price_kopeks(current_price)}/день\n\n'
'Введите новую цену за день в рублях.\n'
'Пример: <code>50</code> или <code>99.90</code>',
@@ -1011,7 +1013,7 @@ async def start_edit_tariff_name(
await state.update_data(tariff_id=tariff_id, language=db_user.language)
await callback.message.edit_text(
f'✏️ <b>Редактирование названия</b>\n\nТекущее название: <b>{tariff.name}</b>\n\nВведите новое название:',
f'✏️ <b>Редактирование названия</b>\n\nТекущее название: <b>{html.escape(tariff.name)}</b>\n\nВведите новое название:',
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text=texts.CANCEL, callback_data=f'admin_tariff_view:{tariff_id}')]]
),
@@ -1801,7 +1803,7 @@ async def start_edit_tariff_traffic_topup(
buttons.append([InlineKeyboardButton(text=texts.BACK, callback_data=f'admin_tariff_view:{tariff_id}')])
await callback.message.edit_text(
f'📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n'
f'📈 <b>Докупка трафика для «{html.escape(tariff.name)}»</b>\n\n'
f'Статус: {status}\n\n'
f'<b>Пакеты:</b>\n{packages_display}\n\n'
f'<b>Макс. лимит:</b> {max_limit_display}\n\n'
@@ -1887,7 +1889,7 @@ async def toggle_tariff_traffic_topup(
try:
await callback.message.edit_text(
f'📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n'
f'📈 <b>Докупка трафика для «{html.escape(tariff.name)}»</b>\n\n'
f'Статус: {status}\n\n'
f'<b>Пакеты:</b>\n{packages_display}\n\n'
f'<b>Макс. лимит:</b> {max_limit_display}\n\n'
@@ -1931,7 +1933,7 @@ async def start_edit_traffic_topup_packages(
await callback.message.edit_text(
f'📦 <b>Настройка пакетов докупки трафика</b>\n\n'
f'Тариф: <b>{tariff.name}</b>\n\n'
f'Тариф: <b>{html.escape(tariff.name)}</b>\n\n'
f'<b>Текущие пакеты:</b>\n{packages_display}\n\n'
'Введите пакеты в формате:\n'
f'<code>{current_packages}</code>\n\n'
@@ -2010,7 +2012,7 @@ async def process_edit_traffic_topup_packages(
await message.answer(
f'✅ <b>Пакеты обновлены!</b>\n\n'
f'📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n'
f'📈 <b>Докупка трафика для «{html.escape(tariff.name)}»</b>\n\n'
f'Статус: ✅ Включено\n\n'
f'<b>Пакеты:</b>\n{packages_display}\n\n'
f'<b>Макс. лимит:</b> {max_limit_display}\n\n'
@@ -2051,7 +2053,7 @@ async def start_edit_max_topup_traffic(
await callback.message.edit_text(
f'📊 <b>Максимальный лимит трафика</b>\n\n'
f'Тариф: <b>{tariff.name}</b>\n'
f'Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'Текущий лимит: <b>{current_display}</b>\n\n'
f'Введите максимальный общий объем трафика (в ГБ), который может быть на подписке после всех докупок.\n\n'
f'• Например, если тариф дает 100 ГБ и лимит 200 ГБ — пользователь сможет докупить еще 100 ГБ\n'
@@ -2127,7 +2129,7 @@ async def process_edit_max_topup_traffic(
await message.answer(
f'✅ <b>Лимит обновлен!</b>\n\n'
f'📈 <b>Докупка трафика для «{tariff.name}»</b>\n\n'
f'📈 <b>Докупка трафика для «{html.escape(tariff.name)}»</b>\n\n'
f'Статус: ✅ Включено\n\n'
f'<b>Пакеты:</b>\n{packages_display}\n\n'
f'<b>Макс. лимит:</b> {max_limit_display}\n\n'
@@ -2163,7 +2165,7 @@ async def confirm_delete_tariff(
warning = f'\n\n⚠️ <b>Внимание!</b> На этом тарифе {subs_count} подписок.\nОни будут отвязаны от тарифа.'
await callback.message.edit_text(
f'🗑️ <b>Удаление тарифа</b>\n\nВы действительно хотите удалить тариф <b>{tariff.name}</b>?{warning}',
f'🗑️ <b>Удаление тарифа</b>\n\nВы действительно хотите удалить тариф <b>{html.escape(tariff.name)}</b>?{warning}',
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
@@ -2278,7 +2280,7 @@ async def start_edit_tariff_squads(
selected_count = len(current_squads)
await callback.message.edit_text(
f'🌐 <b>Серверы для тарифа «{tariff.name}»</b>\n\n'
f'🌐 <b>Серверы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: {selected_count} из {len(squads)}\n\n'
'Если не выбран ни один сервер - доступны все.\n'
'Нажмите на сервер для выбора/отмены:',
@@ -2341,7 +2343,7 @@ async def toggle_tariff_squad(
try:
await callback.message.edit_text(
f'🌐 <b>Серверы для тарифа «{tariff.name}»</b>\n\n'
f'🌐 <b>Серверы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: {len(current_squads)} из {len(squads)}\n\n'
'Если не выбран ни один сервер - доступны все.\n'
'Нажмите на сервер для выбора/отмены:',
@@ -2406,7 +2408,7 @@ async def clear_tariff_squads(
try:
await callback.message.edit_text(
f'🌐 <b>Серверы для тарифа «{tariff.name}»</b>\n\n'
f'🌐 <b>Серверы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: 0 из {len(squads)}\n\n'
'Если не выбран ни один сервер - доступны все.\n'
'Нажмите на сервер для выбора/отмены:',
@@ -2470,7 +2472,7 @@ async def select_all_tariff_squads(
try:
await callback.message.edit_text(
f'🌐 <b>Серверы для тарифа «{tariff.name}»</b>\n\n'
f'🌐 <b>Серверы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: {len(squads)} из {len(squads)}\n\n'
'Если не выбран ни один сервер - доступны все.\n'
'Нажмите на сервер для выбора/отмены:',
@@ -2540,7 +2542,7 @@ async def start_edit_tariff_promo_groups(
selected_count = len(current_groups)
await callback.message.edit_text(
f'👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\n\n'
f'👥 <b>Промогруппы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: {selected_count}\n\n'
'Если не выбрана ни одна группа - тариф доступен всем.\n'
'Выберите группы, которым доступен этот тариф:',
@@ -2608,7 +2610,7 @@ async def toggle_tariff_promo_group(
try:
await callback.message.edit_text(
f'👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\n\n'
f'👥 <b>Промогруппы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: {len(current_groups)}\n\n'
'Если не выбрана ни одна группа - тариф доступен всем.\n'
'Выберите группы, которым доступен этот тариф:',
@@ -2665,7 +2667,7 @@ async def clear_tariff_promo_groups(
try:
await callback.message.edit_text(
f'👥 <b>Промогруппы для тарифа «{tariff.name}»</b>\n\n'
f'👥 <b>Промогруппы для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Выбрано: 0\n\n'
'Если не выбрана ни одна группа - тариф доступен всем.\n'
'Выберите группы, которым доступен этот тариф:',
@@ -2731,7 +2733,7 @@ async def start_edit_traffic_reset_mode(
current_mode = getattr(tariff, 'traffic_reset_mode', None)
await callback.message.edit_text(
f'🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n'
f'🔄 <b>Режим сброса трафика для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Текущий режим: {_format_traffic_reset_mode(current_mode)}\n\n'
'Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n'
'• <b>Глобальная настройка</b> — использовать значение из конфига бота\n'
@@ -2775,7 +2777,7 @@ async def set_traffic_reset_mode(
# Обновляем клавиатуру
await callback.message.edit_text(
f'🔄 <b>Режим сброса трафика для тарифа «{tariff.name}»</b>\n\n'
f'🔄 <b>Режим сброса трафика для тарифа «{html.escape(tariff.name)}»</b>\n\n'
f'Текущий режим: {mode_display}\n\n'
'Выберите, когда сбрасывать использованный трафик у подписчиков этого тарифа:\n\n'
'• <b>Глобальная настройка</b> — использовать значение из конфига бота\n'
+10 -8
View File
@@ -232,8 +232,10 @@ async def view_admin_ticket(
TicketStatus.PENDING.value: texts.t('TICKET_STATUS_PENDING', 'В ожидании'),
}.get(ticket.status, ticket.status)
user_name = ticket.user.full_name if ticket.user else 'Unknown'
telegram_id_display = (ticket.user.telegram_id or ticket.user.email or f'#{ticket.user.id}') if ticket.user else ''
user_name = html.escape(ticket.user.full_name) if ticket.user else 'Unknown'
telegram_id_display = (
html.escape(str(ticket.user.telegram_id or ticket.user.email or f'#{ticket.user.id}')) if ticket.user else ''
)
username_value = ticket.user.username if ticket.user else None
id_label = 'Telegram ID' if (ticket.user and ticket.user.telegram_id) else 'ID'
@@ -245,7 +247,7 @@ async def view_admin_ticket(
header += f'📱 Username: @{safe_username}\n'
else:
header += '📱 Username: отсутствует\n'
header += f'📝 Заголовок: {ticket.title}\n'
header += f'📝 Заголовок: {html.escape(ticket.title)}\n'
header += f'📊 Статус: {ticket.status_emoji} {status_text}\n'
header += f'📅 Создан: {ticket.created_at.strftime("%d.%m.%Y %H:%M")}\n\n'
@@ -261,7 +263,7 @@ async def view_admin_ticket(
message_blocks.append(f'💬 Сообщения ({len(ticket.messages)}):\n\n')
for msg in ticket.messages:
sender = '👤 Пользователь' if msg.is_user_message else '🛠️ Поддержка'
block = f'{sender} ({msg.created_at.strftime("%d.%m %H:%M")}):\n{msg.message_text}\n\n'
block = f'{sender} ({msg.created_at.strftime("%d.%m %H:%M")}):\n{html.escape(msg.message_text)}\n\n'
if getattr(msg, 'has_media', False) and getattr(msg, 'media_type', None) == 'photo':
block += '📎 Вложение: фото\n\n'
message_blocks.append(block)
@@ -801,10 +803,10 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
TicketStatus.CLOSED.value: texts.t('TICKET_STATUS_CLOSED', 'Закрыт'),
TicketStatus.PENDING.value: texts.t('TICKET_STATUS_PENDING', 'В ожидании'),
}.get(updated.status, updated.status)
user_name = updated.user.full_name if updated.user else 'Unknown'
user_name = html.escape(updated.user.full_name) if updated.user else 'Unknown'
ticket_text = f'🎫 Тикет #{updated.id}\n\n'
ticket_text += f'👤 Пользователь: {user_name}\n'
ticket_text += f'📝 Заголовок: {updated.title}\n'
ticket_text += f'📝 Заголовок: {html.escape(updated.title)}\n'
ticket_text += f'📊 Статус: {updated.status_emoji} {status_text}\n'
ticket_text += f'📅 Создан: {updated.created_at.strftime("%d.%m.%Y %H:%M")}\n'
ticket_text += f'🔄 Обновлен: {updated.updated_at.strftime("%d.%m.%Y %H:%M")}\n'
@@ -823,7 +825,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
ticket_text += f'🔗 Чат по ID: <a href="{chat_link}">{chat_link}</a>\n'
elif updated.user:
# Email-only user
user_id_display = updated.user.email or f'#{updated.user.id}'
user_id_display = html.escape(str(updated.user.email or f'#{updated.user.id}'))
ticket_text += f'🆔 ID: <code>{user_id_display}</code>\n'
ticket_text += '📧 Тип: Email-пользователь\n'
ticket_text += '\n'
@@ -837,7 +839,7 @@ async def handle_admin_block_duration_input(message: types.Message, state: FSMCo
for msg in updated.messages:
sender = '👤 Пользователь' if msg.is_user_message else '🛠️ Поддержка'
ticket_text += f'{sender} ({msg.created_at.strftime("%d.%m %H:%M")}):\n'
ticket_text += f'{msg.message_text}\n\n'
ticket_text += f'{html.escape(msg.message_text)}\n\n'
if getattr(msg, 'has_media', False) and getattr(msg, 'media_type', None) == 'photo':
ticket_text += '📎 Вложение: фото\n\n'
+61 -109
View File
@@ -47,6 +47,7 @@ from app.services.user_service import UserService
from app.states import AdminStates
from app.utils.decorators import admin_required, error_handler
from app.utils.formatters import format_datetime, format_time_ago
from app.utils.formatting import user_html_link
from app.utils.subscription_utils import (
resolve_hwid_device_limit_for_payload,
)
@@ -822,12 +823,8 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
subscription = profile['subscription']
text = '📱 <b>Подписка и настройки пользователя</b>\n\n'
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
user_id_display = user.telegram_id
else:
user_link = f'<b>{user.full_name}</b>'
user_id_display = user.email or f'#{user.id}'
user_link = user_html_link(user)
user_id_display = user.telegram_id or user.email or f'#{user.id}'
text += f'👤 {user_link} (ID: <code>{user_id_display}</code>)\n\n'
keyboard = []
@@ -849,7 +846,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db:
if subscription.tariff_id:
tariff = await get_tariff_by_id(db, subscription.tariff_id)
if tariff:
text += f'<b>Тариф:</b> 📦 {tariff.name}\n'
text += f'<b>Тариф:</b> 📦 {html.escape(tariff.name)}\n'
else:
text += f'<b>Тариф:</b> ID {subscription.tariff_id} (удалён)\n'
@@ -961,12 +958,8 @@ async def show_user_transactions(callback: types.CallbackQuery, db_user: User, d
transactions = await get_user_transactions(db, user_id, limit=10)
text = '💳 <b>Транзакции пользователя</b>\n\n'
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
user_id_display = user.telegram_id
else:
user_link = f'<b>{user.full_name}</b>'
user_id_display = user.email or f'#{user.id}'
user_link = user_html_link(user)
user_id_display = user.telegram_id or user.email or f'#{user.id}'
text += f'👤 {user_link} (ID: <code>{user_id_display}</code>)\n'
text += f'💰 Текущий баланс: {settings.format_price(user.balance_kopeks)}\n\n'
@@ -976,7 +969,7 @@ async def show_user_transactions(callback: types.CallbackQuery, db_user: User, d
for transaction in transactions:
type_emoji = '📈' if transaction.amount_kopeks > 0 else '📉'
text += f'{type_emoji} {settings.format_price(abs(transaction.amount_kopeks))}\n'
text += f'📋 {transaction.description}\n'
text += f'📋 {html.escape(transaction.description or "")}\n'
text += f'📅 {format_datetime(transaction.created_at)}\n\n'
else:
text += '📭 <b>Транзакции отсутствуют</b>'
@@ -1057,7 +1050,7 @@ async def process_user_search(message: types.Message, db_user: User, state: FSMC
if not search_results['users']:
await message.answer(
f"🔍 По запросу '<b>{query}</b>' ничего не найдено",
f"🔍 По запросу '<b>{html.escape(query)}</b>' ничего не найдено",
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[[types.InlineKeyboardButton(text='⬅️ Назад', callback_data='admin_users')]]
),
@@ -1065,7 +1058,7 @@ async def process_user_search(message: types.Message, db_user: User, state: FSMC
await state.clear()
return
text = f"🔍 <b>Результаты поиска:</b> '{query}'\n\n"
text = f"🔍 <b>Результаты поиска:</b> '{html.escape(query)}'\n\n"
text += 'Выберите пользователя:'
keyboard = []
@@ -1175,7 +1168,7 @@ async def show_user_management(callback: types.CallbackQuery, db_user: User, db:
sections = [
texts.ADMIN_USER_MANAGEMENT_PROFILE.format(
name=user.full_name,
name=html.escape(user.full_name),
telegram_id=user.telegram_id,
username=username_display,
status=status_text,
@@ -1223,11 +1216,11 @@ async def show_user_management(callback: types.CallbackQuery, db_user: User, db:
texts.t(
'ADMIN_USER_PROMO_GROUPS_PRIMARY',
'⭐ Основная: {name} (Priority: {priority})',
).format(name=primary_group.name, priority=getattr(primary_group, 'priority', 0))
).format(name=html.escape(primary_group.name), priority=getattr(primary_group, 'priority', 0))
)
sections.append(
texts.ADMIN_USER_MANAGEMENT_PROMO_GROUP.format(
name=primary_group.name,
name=html.escape(primary_group.name),
server_discount=primary_group.server_discount_percent,
traffic_discount=primary_group.traffic_discount_percent,
device_discount=primary_group.device_discount_percent,
@@ -1249,7 +1242,7 @@ async def show_user_management(callback: types.CallbackQuery, db_user: User, db:
)
)
for group in additional_groups:
sections.append(f'{group.name} (Priority: {getattr(group, "priority", 0)})')
sections.append(f'{html.escape(group.name)} (Priority: {getattr(group, "priority", 0)})')
else:
sections.append(texts.ADMIN_USER_MANAGEMENT_PROMO_GROUP_NONE)
@@ -1264,7 +1257,7 @@ async def show_user_management(callback: types.CallbackQuery, db_user: User, db:
restriction_lines.append(' • 🚫 Продление/покупка запрещена')
restriction_reason = getattr(user, 'restriction_reason', None)
if restriction_reason:
restriction_lines.append(f' 📝 Причина: {restriction_reason}')
restriction_lines.append(f' 📝 Причина: {html.escape(restriction_reason)}')
sections.append('\n'.join(restriction_lines))
text = '\n\n'.join(sections)
@@ -1321,7 +1314,7 @@ async def _build_user_referrals_view(
'ADMIN_USER_REFERRALS_SUMMARY',
'👤 {name} (ID: <code>{telegram_id}</code>)\n👥 Всего рефералов: {count}',
).format(
name=user.full_name,
name=html.escape(user.full_name),
telegram_id=user.telegram_id,
count=len(referrals),
)
@@ -1356,11 +1349,12 @@ async def _build_user_referrals_view(
items = []
for referral in referrals[:limit]:
username_part = f', @{referral.username}' if referral.username else ''
safe_name = html.escape(referral.full_name)
if referral.telegram_id:
referral_link = f'<a href="tg://user?id={referral.telegram_id}">{referral.full_name}</a>'
referral_link = f'<a href="tg://user?id={referral.telegram_id}">{safe_name}</a>'
referral_id_display = referral.telegram_id
else:
referral_link = f'<b>{referral.full_name}</b>'
referral_link = f'<b>{safe_name}</b>'
referral_id_display = referral.email or f'#{referral.id}'
items.append(
texts.t(
@@ -1737,7 +1731,7 @@ async def start_edit_user_referrals(
'Или нажмите кнопку ниже, чтобы отменить.'
),
).format(
name=user.full_name,
name=html.escape(user.full_name),
telegram_id=user.telegram_id,
)
@@ -1972,7 +1966,7 @@ async def _render_user_promo_group(message: types.Message, language: str, user:
current_line = texts.t(
'ADMIN_USER_PROMO_GROUPS_PRIMARY',
'⭐ Основная: {name} (Priority: {priority})',
).format(name=primary_group.name, priority=getattr(primary_group, 'priority', 0))
).format(name=html.escape(primary_group.name), priority=getattr(primary_group, 'priority', 0))
discount_line = texts.ADMIN_USER_PROMO_GROUP_DISCOUNTS.format(
servers=primary_group.server_discount_percent,
@@ -1997,7 +1991,7 @@ async def _render_user_promo_group(message: types.Message, language: str, user:
+ '\n'
)
for group in additional_groups:
additional_line += f'{group.name} (Priority: {getattr(group, "priority", 0)})\n'
additional_line += f'{html.escape(group.name)} (Priority: {getattr(group, "priority", 0)})\n'
discount_line += additional_line
else:
current_line = texts.t(
@@ -2388,7 +2382,7 @@ async def show_user_restrictions(callback: types.CallbackQuery, db_user: User, d
text_lines = [
'⚠️ <b>Ограничения пользователя</b>',
f'👤 {user.full_name}',
f'👤 {html.escape(user.full_name)}',
'',
'✅ — разрешено, 🚫 — запрещено',
'',
@@ -2398,7 +2392,7 @@ async def show_user_restrictions(callback: types.CallbackQuery, db_user: User, d
if restriction_reason:
text_lines.append('')
text_lines.append(f'📝 <b>Причина:</b> {restriction_reason}')
text_lines.append(f'📝 <b>Причина:</b> {html.escape(restriction_reason)}')
keyboard = get_user_restrictions_keyboard(
user_id=user_id,
@@ -2479,7 +2473,7 @@ async def ask_restriction_reason(callback: types.CallbackQuery, db_user: User, d
'выполнить запрещённое действие.\n\n'
)
if current_reason:
text += f'Текущая причина: <i>{current_reason}</i>\n\n'
text += f'Текущая причина: <i>{html.escape(current_reason)}</i>\n\n'
text += 'Отправьте новую причину или /cancel для отмены:'
await callback.message.edit_text(
@@ -2525,12 +2519,12 @@ async def save_restriction_reason(message: types.Message, db_user: User, db: Asy
'✅ <b>Причина ограничения сохранена</b>',
'',
'⚠️ <b>Ограничения пользователя</b>',
f'👤 {user.full_name}',
f'👤 {html.escape(user.full_name)}',
'',
f'{"🚫" if restriction_topup else ""} Пополнение баланса',
f'{"🚫" if restriction_subscription else ""} Продление/покупка подписки',
'',
f'📝 <b>Причина:</b> {reason}',
f'📝 <b>Причина:</b> {html.escape(reason)}',
]
keyboard = get_user_restrictions_keyboard(
@@ -2596,12 +2590,8 @@ async def show_inactive_users(callback: types.CallbackQuery, db_user: User, db:
text += '\n'
for user in inactive_users[:10]:
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
user_id_display = user.telegram_id
else:
user_link = f'<b>{user.full_name}</b>'
user_id_display = user.email or f'#{user.id}'
user_link = user_html_link(user)
user_id_display = user.telegram_id or user.email or f'#{user.id}'
has_active = user.subscription and user.subscription.is_active
sub_badge = ' 🛡️' if has_active else ''
text += f'👤 {user_link}{sub_badge}\n'
@@ -2691,12 +2681,8 @@ async def show_user_statistics(callback: types.CallbackQuery, db_user: User, db:
campaign_stats = await get_campaign_statistics(db, campaign_registration.campaign_id)
text = '📊 <b>Статистика пользователя</b>\n\n'
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
user_id_display = user.telegram_id
else:
user_link = f'<b>{user.full_name}</b>'
user_id_display = user.email or f'#{user.id}'
user_link = user_html_link(user)
user_id_display = user.telegram_id or user.email or f'#{user.id}'
text += f'👤 {user_link} (ID: <code>{user_id_display}</code>)\n\n'
text += '<b>Основная информация:</b>\n'
@@ -2721,13 +2707,13 @@ async def show_user_statistics(callback: types.CallbackQuery, db_user: User, db:
if user.referred_by_id:
referrer = await get_user_by_id(db, user.referred_by_id)
if referrer:
text += f'• Пришел по реферальной ссылке от <b>{referrer.full_name}</b>\n'
text += f'• Пришел по реферальной ссылке от <b>{html.escape(referrer.full_name)}</b>\n'
else:
text += '• Пришел по реферальной ссылке (реферер не найден)\n'
if campaign_registration and campaign_registration.campaign:
text += f'• Дополнительно зарегистрирован через кампанию <b>{campaign_registration.campaign.name}</b>\n'
text += f'• Дополнительно зарегистрирован через кампанию <b>{html.escape(campaign_registration.campaign.name)}</b>\n'
elif campaign_registration and campaign_registration.campaign:
text += f'• Регистрация через рекламную кампанию <b>{campaign_registration.campaign.name}</b>\n'
text += f'• Регистрация через рекламную кампанию <b>{html.escape(campaign_registration.campaign.name)}</b>\n'
if campaign_registration.created_at:
text += f'• Дата регистрации по кампании: {campaign_registration.created_at.strftime("%d.%m.%Y %H:%M")}\n'
else:
@@ -2737,7 +2723,7 @@ async def show_user_statistics(callback: types.CallbackQuery, db_user: User, db:
if campaign_registration and campaign_registration.campaign and campaign_stats:
text += '<b>Рекламная кампания:</b>\n'
text += f'• Название: <b>{campaign_registration.campaign.name}</b>'
text += f'• Название: <b>{html.escape(campaign_registration.campaign.name)}</b>'
if campaign_registration.campaign.start_parameter:
text += f' (параметр: <code>{campaign_registration.campaign.start_parameter}</code>)'
text += '\n'
@@ -2771,7 +2757,7 @@ async def show_user_statistics(callback: types.CallbackQuery, db_user: User, db:
if referral_stats['referrals_detail']:
text += '\n<b>Детали по рефералам:</b>\n'
for detail in referral_stats['referrals_detail'][:5]:
referral_name = detail['referral_name']
referral_name = html.escape(detail['referral_name'])
earned = settings.format_price(detail['total_earned_kopeks'])
status = '🟢' if detail['is_active'] else '🔴'
text += f'{status} {referral_name}: {earned}\n'
@@ -4226,7 +4212,7 @@ async def change_subscription_type(callback: types.CallbackQuery, db_user: User,
current_type = '🎁 Триал' if subscription.is_trial else '💎 Платная'
text = '🔄 <b>Смена типа подписки</b>\n\n'
text += f'👤 {profile["user"].full_name}\n'
text += f'👤 {html.escape(profile["user"].full_name)}\n'
text += f'📱 Текущий тип: {current_type}\n\n'
text += 'Выберите новый тип подписки:'
@@ -4307,12 +4293,8 @@ async def admin_buy_subscription(callback: types.CallbackQuery, db_user: User, d
)
text = '💳 <b>Покупка подписки для пользователя</b>\n\n'
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
text += f'👤 {target_user_link} (ID: {target_user_id_display})\n'
text += f'💰 Баланс пользователя: {settings.format_price(target_user.balance_kopeks)}\n\n'
traffic_text = 'Безлимит' if (subscription.traffic_limit_gb or 0) <= 0 else f'{subscription.traffic_limit_gb} ГБ'
@@ -4400,12 +4382,8 @@ async def admin_buy_subscription_confirm(callback: types.CallbackQuery, db_user:
return
text = '💳 <b>Подтверждение покупки подписки</b>\n\n'
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
text += f'👤 {target_user_link} (ID: {target_user_id_display})\n'
text += f'📅 Период подписки: {period_days} дней\n'
text += f'💰 Стоимость: {settings.format_price(price_kopeks)}\n'
@@ -4646,12 +4624,8 @@ async def admin_buy_subscription_execute(callback: types.CallbackQuery, db_user:
else:
message = '❌ Ошибка: у пользователя нет существующей подписки'
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
await callback.message.edit_text(
f'{message}\n\n'
f'👤 {target_user_link} (ID: {target_user_id_display})\n'
@@ -4727,12 +4701,8 @@ async def admin_buy_tariff(callback: types.CallbackQuery, db_user: User, db: Asy
await callback.answer()
return
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
text = '💳 <b>Покупка тарифа для пользователя</b>\n\n'
text += f'👤 {target_user_link} (ID: {target_user_id_display})\n'
text += f'💰 Баланс: {settings.format_price(target_user.balance_kopeks)}\n\n'
@@ -4742,7 +4712,7 @@ async def admin_buy_tariff(callback: types.CallbackQuery, db_user: User, db: Asy
traffic = '♾️' if tariff.traffic_limit_gb == 0 else f'{tariff.traffic_limit_gb} ГБ'
prices = tariff.period_prices or {}
min_price = min(prices.values()) if prices else 0
text += f'<b>{tariff.name}</b> — {traffic} / {tariff.device_limit} 📱 от {settings.format_price(min_price)}\n'
text += f'<b>{html.escape(tariff.name)}</b> — {traffic} / {tariff.device_limit} 📱 от {settings.format_price(min_price)}\n'
keyboard = []
for tariff in tariffs:
@@ -4787,18 +4757,14 @@ async def admin_buy_tariff_period(callback: types.CallbackQuery, db_user: User,
await callback.answer('❌ Тариф недоступен', show_alert=True)
return
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
traffic = '♾️ Безлимит' if tariff.traffic_limit_gb == 0 else f'{tariff.traffic_limit_gb} ГБ'
text = '💳 <b>Покупка тарифа для пользователя</b>\n\n'
text += f'👤 {target_user_link} (ID: {target_user_id_display})\n'
text += f'💰 Баланс: {settings.format_price(target_user.balance_kopeks)}\n\n'
text += f'📦 <b>Тариф: {tariff.name}</b>\n'
text += f'📦 <b>Тариф: {html.escape(tariff.name)}</b>\n'
text += f'📊 Трафик: {traffic}\n'
text += f'📱 Устройств: {tariff.device_limit}\n'
text += f'🌐 Серверов: {len(tariff.allowed_squads) if tariff.allowed_squads else 0}\n\n'
@@ -4876,18 +4842,14 @@ async def admin_buy_tariff_confirm(callback: types.CallbackQuery, db_user: User,
await callback.answer()
return
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
traffic = '♾️ Безлимит' if tariff.traffic_limit_gb == 0 else f'{tariff.traffic_limit_gb} ГБ'
text = '💳 <b>Подтверждение покупки тарифа</b>\n\n'
text += f'👤 {target_user_link} (ID: {target_user_id_display})\n'
text += f'💰 Баланс: {settings.format_price(target_user.balance_kopeks)}\n\n'
text += f'📦 <b>Тариф: {tariff.name}</b>\n'
text += f'📦 <b>Тариф: {html.escape(tariff.name)}</b>\n'
text += f'📊 Трафик: {traffic}\n'
text += f'📱 Устройств: {tariff.device_limit}\n'
text += f'📅 Период: {period} дней\n'
@@ -5055,18 +5017,14 @@ async def admin_buy_tariff_execute(callback: types.CallbackQuery, db_user: User,
description=f'Покупка тарифа {tariff.name} на {period} дней (администратор)',
)
if target_user.telegram_id:
target_user_link = f'<a href="tg://user?id={target_user.telegram_id}">{target_user.full_name}</a>'
target_user_id_display = target_user.telegram_id
else:
target_user_link = f'<b>{target_user.full_name}</b>'
target_user_id_display = target_user.email or f'#{target_user.id}'
target_user_link = user_html_link(target_user)
target_user_id_display = target_user.telegram_id or target_user.email or f'#{target_user.id}'
traffic = '♾️ Безлимит' if tariff.traffic_limit_gb == 0 else f'{tariff.traffic_limit_gb} ГБ'
await callback.message.edit_text(
f'✅ <b>Тариф успешно куплен!</b>\n\n'
f'👤 {target_user_link} (ID: {target_user_id_display})\n'
f'📦 Тариф: {tariff.name}\n'
f'📦 Тариф: {html.escape(tariff.name)}\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {period} дней\n'
@@ -5090,7 +5048,7 @@ async def admin_buy_tariff_execute(callback: types.CallbackQuery, db_user: User,
await callback.bot.send_message(
chat_id=target_user.telegram_id,
text=f'💳 <b>Администратор оформил вам тариф</b>\n\n'
f'📦 Тариф: {tariff.name}\n'
f'📦 Тариф: {html.escape(tariff.name)}\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {period} дней\n'
@@ -5234,14 +5192,11 @@ async def show_admin_tariff_change(callback: types.CallbackQuery, db_user: User,
current_tariff = await get_tariff_by_id(db, subscription.tariff_id)
text = '📦 <b>Смена тарифа пользователя</b>\n\n'
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
else:
user_link = f'<b>{user.full_name}</b> ({user.email or f"#{user.id}"})'
user_link = user_html_link(user)
text += f'👤 {user_link}\n\n'
if current_tariff:
text += f'<b>Текущий тариф:</b> {current_tariff.name}\n\n'
text += f'<b>Текущий тариф:</b> {html.escape(current_tariff.name)}\n\n'
else:
text += '<b>Текущий тариф:</b> не установлен\n\n'
@@ -5307,12 +5262,9 @@ async def select_admin_tariff_change(callback: types.CallbackQuery, db_user: Use
servers_count = len(tariff.allowed_squads) if tariff.allowed_squads else 0
text = '📦 <b>Подтверждение смены тарифа</b>\n\n'
if user.telegram_id:
user_link = f'<a href="tg://user?id={user.telegram_id}">{user.full_name}</a>'
else:
user_link = f'<b>{user.full_name}</b> ({user.email or f"#{user.id}"})'
user_link = user_html_link(user)
text += f'👤 {user_link}\n\n'
text += f'<b>Новый тариф:</b> {tariff.name}\n'
text += f'<b>Новый тариф:</b> {html.escape(tariff.name)}\n'
text += f'• Устройства: {tariff.device_limit}\n'
text += f'• Трафик: {traffic_str}\n'
text += f'• Серверы: {servers_count}\n\n'
@@ -5431,7 +5383,7 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
await callback.message.edit_text(
f'✅ <b>Тариф успешно изменен</b>\n\n'
f'Новый тариф: <b>{tariff.name}</b>\n'
f'Новый тариф: <b>{html.escape(tariff.name)}</b>\n'
f'• Устройства: {subscription.device_limit}\n'
f'• Трафик: {"♾️" if tariff.traffic_limit_gb == 0 else f"{tariff.traffic_limit_gb} ГБ"}\n'
f'• Серверы: {len(tariff.allowed_squads) if tariff.allowed_squads else 0}',
@@ -5451,7 +5403,7 @@ async def confirm_admin_tariff_change(callback: types.CallbackQuery, db_user: Us
await db.rollback()
await callback.message.edit_text(
f'❌ <b>Ошибка смены тарифа</b>\n\nДетали: {e!s}',
f'❌ <b>Ошибка смены тарифа</b>\n\nДетали: {html.escape(str(e))}',
reply_markup=types.InlineKeyboardMarkup(
inline_keyboard=[
[
+4 -2
View File
@@ -1,5 +1,7 @@
"""Handler for CloudPayments balance top-up."""
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -137,7 +139,7 @@ async def process_cloudpayments_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -203,7 +205,7 @@ async def start_cloudpayments_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -21,7 +23,7 @@ async def start_cryptobot_payment(callback: types.CallbackQuery, db_user: User,
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -85,7 +87,7 @@ async def process_cryptobot_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+2 -2
View File
@@ -161,7 +161,7 @@ async def process_freekassa_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -250,7 +250,7 @@ async def _start_freekassa_topup_impl(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+3 -2
View File
@@ -1,3 +1,4 @@
import html
from datetime import UTC, datetime
import structlog
@@ -28,7 +29,7 @@ async def start_heleket_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -99,7 +100,7 @@ async def process_heleket_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,5 +1,7 @@
"""Handler for KassaAI balance top-up."""
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -45,7 +47,7 @@ async def _check_topup_restriction(callback: types.CallbackQuery, db_user: User)
if not getattr(db_user, 'restriction_topup', False):
return False
texts = get_texts(db_user.language)
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -173,7 +175,7 @@ async def process_kassa_ai_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import Dispatcher, F, types
from aiogram.exceptions import TelegramBadRequest
@@ -233,7 +235,7 @@ async def show_balance_history(callback: types.CallbackQuery, db_user: User, db:
)
text += f'{emoji} {amount_text}\n'
text += f'📝 {transaction.description}\n'
text += f'📝 {html.escape(transaction.description or "")}\n'
text += f'📅 {transaction.created_at.strftime("%d.%m.%Y %H:%M")}\n\n'
keyboard = []
@@ -266,7 +268,7 @@ async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db:
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -25,7 +27,7 @@ async def start_mulenpay_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -92,7 +94,7 @@ async def process_mulenpay_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+2 -2
View File
@@ -262,7 +262,7 @@ async def start_pal24_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -330,7 +330,7 @@ async def process_pal24_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,5 +1,7 @@
"""Handlers for Platega balance interactions."""
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -98,7 +100,7 @@ async def start_platega_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -201,7 +203,7 @@ async def process_platega_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+4 -2
View File
@@ -1,5 +1,7 @@
"""Handler for RioPay balance top-up."""
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -142,7 +144,7 @@ async def process_riopay_payment_amount(
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
await message.answer(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
@@ -202,7 +204,7 @@ async def start_riopay_topup(
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
await callback.message.edit_text(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
+4 -2
View File
@@ -1,5 +1,7 @@
"""Handler for SeverPay balance top-up."""
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -142,7 +144,7 @@ async def process_severpay_payment_amount(
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
await message.answer(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
@@ -202,7 +204,7 @@ async def start_severpay_topup(
restriction_kb = _check_topup_restriction(db_user, texts)
if restriction_kb:
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
await callback.message.edit_text(
f'🚫 <b>Пополнение ограничено</b>\n\n{reason}',
parse_mode='HTML',
+4 -2
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import types
from aiogram.fsm.context import FSMContext
@@ -25,7 +27,7 @@ async def start_stars_payment(callback: types.CallbackQuery, db_user: User, stat
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -62,7 +64,7 @@ async def process_stars_payment_amount(message: types.Message, db_user: User, am
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+3 -1
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import types
@@ -19,7 +21,7 @@ async def start_tribute_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+3 -2
View File
@@ -1,3 +1,4 @@
import html
from datetime import UTC, datetime
import structlog
@@ -28,7 +29,7 @@ async def start_wata_payment(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -88,7 +89,7 @@ async def process_wata_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+5 -4
View File
@@ -1,3 +1,4 @@
import html
from datetime import UTC, datetime
import structlog
@@ -24,7 +25,7 @@ async def start_yookassa_payment(callback: types.CallbackQuery, db_user: User, s
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -70,7 +71,7 @@ async def start_yookassa_sbp_payment(callback: types.CallbackQuery, db_user: Use
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -118,7 +119,7 @@ async def process_yookassa_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -273,7 +274,7 @@ async def process_yookassa_sbp_payment_amount(
# Проверка ограничения на пополнение
if getattr(db_user, 'restriction_topup', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+3 -2
View File
@@ -1183,12 +1183,13 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
if tariff:
is_daily_tariff = getattr(tariff, 'is_daily', False)
# Формируем краткий блок информации о тарифе для главного меню
tariff_info_block = f'\n📦 Тариф: {tariff.name}'
tariff_info_block = f'\n📦 Тариф: {html.escape(tariff.name)}'
except Exception as e:
logger.debug('Не удалось загрузить тариф для главного меню', error=e)
base_text = texts.MAIN_MENU.format(
user_name=user.full_name, subscription_status=_get_subscription_status(user, texts, is_daily_tariff)
user_name=html.escape(user.full_name or ''),
subscription_status=_get_subscription_status(user, texts, is_daily_tariff),
)
# Добавляем информацию о тарифе перед "Выберите действие"
+2 -1
View File
@@ -1,4 +1,5 @@
import asyncio
import html
from datetime import UTC, datetime
import structlog
@@ -39,7 +40,7 @@ async def _render_question_text(
current=current_index,
total=total,
)
lines = [f'🗳️ <b>{poll_title}</b>', '', header, '', question.text]
lines = [f'🗳️ <b>{html.escape(poll_title)}</b>', '', header, '', html.escape(question.text)]
return '\n'.join(lines)
+1 -1
View File
@@ -44,7 +44,7 @@ async def start_simple_subscription_purchase(
# Проверка ограничения на покупку/продление подписки
if getattr(db_user, 'restriction_subscription', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
+2 -1
View File
@@ -1,3 +1,4 @@
import html
from decimal import ROUND_HALF_UP, Decimal
import structlog
@@ -136,7 +137,7 @@ async def _handle_wheel_spin_payment(
emoji = selected_prize.emoji or '🎁'
await message.answer(
f'🎰 <b>Колесо удачи!</b>\n\n'
f'{emoji} <b>{selected_prize.display_name}</b>\n\n'
f'{emoji} <b>{html.escape(selected_prize.display_name)}</b>\n\n'
f'{prize_message}\n\n'
f'⭐ Потрачено: {stars_amount} Stars',
parse_mode='HTML',
+8 -11
View File
@@ -1,3 +1,4 @@
import html
from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
@@ -117,7 +118,7 @@ async def _activate_pending_gift_after_registration(
gift_purchase.status = GuestPurchaseStatus.PENDING_ACTIVATION.value
await db.flush()
await svc_activate(db, gift_purchase.token, skip_notification=True)
tariff_name = gift_purchase.tariff.name if gift_purchase.tariff else ''
tariff_name = html.escape(gift_purchase.tariff.name) if gift_purchase.tariff else ''
await answer_func(
f'🎁 <b>Подарок активирован!</b>\n'
f'{tariff_name}{gift_purchase.period_days} дн.\n\n'
@@ -322,13 +323,13 @@ async def _apply_campaign_bonus_if_needed(
amount_text = texts.format_price(result.balance_kopeks)
return texts.CAMPAIGN_BONUS_BALANCE.format(
amount=amount_text,
name=campaign.name,
name=html.escape(campaign.name),
)
if result.bonus_type == 'subscription':
traffic_text = texts.format_traffic(result.subscription_traffic_gb or 0)
return texts.CAMPAIGN_BONUS_SUBSCRIPTION.format(
name=campaign.name,
name=html.escape(campaign.name),
days=result.subscription_days,
traffic=traffic_text,
devices=result.subscription_device_limit,
@@ -1461,7 +1462,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
texts.t(
'WELCOME_FALLBACK',
'Добро пожаловать, {user_name}!',
).format(user_name=existing_user.full_name)
).format(user_name=html.escape(existing_user.full_name or ''))
)
await state.clear()
@@ -1695,7 +1696,7 @@ async def complete_registration_from_callback(callback: types.CallbackQuery, sta
texts.t(
'WELCOME_FALLBACK',
'Добро пожаловать, {user_name}!',
).format(user_name=user.full_name)
).format(user_name=html.escape(user.full_name or ''))
)
logger.info('✅ Регистрация завершена для пользователя', telegram_id=user.telegram_id)
@@ -1763,7 +1764,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
texts.t(
'WELCOME_FALLBACK',
'Добро пожаловать, {user_name}!',
).format(user_name=existing_user.full_name)
).format(user_name=html.escape(existing_user.full_name or ''))
)
await state.clear()
@@ -2030,7 +2031,7 @@ async def complete_registration(message: types.Message, state: FSMContext, db: A
texts.t(
'WELCOME_FALLBACK',
'Добро пожаловать, {user_name}!',
).format(user_name=user.full_name)
).format(user_name=html.escape(user.full_name or ''))
)
logger.info('✅ Регистрация завершена для пользователя', telegram_id=user.telegram_id)
@@ -2140,8 +2141,6 @@ def get_referral_code_keyboard(language: str):
async def get_main_menu_text(user, texts, db: AsyncSession):
import html
base_text = texts.MAIN_MENU.format(
user_name=html.escape(user.full_name or ''), subscription_status=_get_subscription_status(user, texts)
)
@@ -2189,8 +2188,6 @@ async def get_main_menu_text(user, texts, db: AsyncSession):
async def get_main_menu_text_simple(user_name, texts, db: AsyncSession):
import html
base_text = texts.MAIN_MENU.format(
user_name=html.escape(user_name or ''), subscription_status=_get_subscription_status_simple(texts)
)
+4 -4
View File
@@ -336,7 +336,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
tariff_type_str = '🔄 Суточный' if is_daily else '📅 Периодный'
tariff_info_lines = [
f'<b>📦 {tariff.name}</b>',
f'<b>📦 {html.escape(tariff.name)}</b>',
f'Тип: {tariff_type_str}',
f'Трафик: {tariff.traffic_limit_gb} ГБ' if tariff.traffic_limit_gb > 0 else 'Трафик: ∞ Безлимит',
f'Устройства: {tariff.device_limit}',
@@ -453,7 +453,7 @@ async def show_subscription_info(callback: types.CallbackQuery, db_user: User, d
device_limit_display = str(subscription.device_limit)
message = message_template.format(
full_name=db_user.full_name,
full_name=html.escape(db_user.full_name or ''),
balance=settings.format_price(db_user.balance_kopeks),
status_emoji=status_emoji,
status_display=status_display,
@@ -761,7 +761,7 @@ async def activate_trial(callback: types.CallbackQuery, db_user: User, db: Async
# Проверка ограничения на покупку/продление подписки
if getattr(db_user, 'restriction_subscription', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
support_url = settings.get_support_contact_url()
keyboard = []
if support_url:
@@ -2068,7 +2068,7 @@ async def devices_continue(callback: types.CallbackQuery, state: FSMContext, db_
async def confirm_purchase(callback: types.CallbackQuery, state: FSMContext, db_user: User, db: AsyncSession):
# Проверка ограничения на покупку/продление подписки
if getattr(db_user, 'restriction_subscription', False):
reason = getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором'
reason = html.escape(getattr(db_user, 'restriction_reason', None) or 'Действие ограничено администратором')
texts = get_texts(db_user.language)
support_url = settings.get_support_contact_url()
keyboard = []
+48 -47
View File
@@ -1,5 +1,6 @@
"""Покупка подписки по тарифам."""
import html
from datetime import UTC, datetime, timedelta
import structlog
@@ -102,11 +103,11 @@ def format_tariffs_list_text(
price_text = f'от {format_price_kopeks(min_price, compact=True)}{discount_icon}'
# Компактный формат: Название — 250 ГБ / 10 📱 от 179₽🔥
lines.append(f'<b>{tariff.name}</b> — {traffic} / {tariff.device_limit} 📱 {price_text}')
lines.append(f'<b>{html.escape(tariff.name)}</b> — {traffic} / {tariff.device_limit} 📱 {price_text}')
# Описание тарифа если есть
if tariff.description:
lines.append(f'<i>{tariff.description}</i>')
lines.append(f'<i>{html.escape(tariff.description)}</i>')
lines.append('')
@@ -238,7 +239,7 @@ def format_tariff_info_for_user(
traffic = format_traffic(tariff.traffic_limit_gb)
text = f"""📦 <b>{tariff.name}</b>
text = f"""📦 <b>{html.escape(tariff.name)}</b>
<b>Параметры:</b>
Трафик: {traffic}
@@ -246,7 +247,7 @@ def format_tariff_info_for_user(
"""
if tariff.description:
text += f'\n📝 {tariff.description}\n'
text += f'\n📝 {html.escape(tariff.description)}\n'
if discount_percent > 0:
text += f'\n🎁 <b>Ваша скидка: {discount_percent}%</b>\n'
@@ -438,7 +439,7 @@ async def format_custom_tariff_preview(
traffic_display = f'{traffic_gb} ГБ' if traffic_gb > 0 else format_traffic(tariff.traffic_limit_gb)
text = f"""📦 <b>{tariff.name}</b>
text = f"""📦 <b>{html.escape(tariff.name)}</b>
<b>Настройте параметры:</b>
"""
@@ -554,7 +555,7 @@ async def select_tariff(
if user_balance >= daily_price:
await callback.message.edit_text(
f'✅ <b>Подтверждение покупки</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'🔄 Тип: <b>Суточный</b>\n\n'
@@ -589,7 +590,7 @@ async def select_tariff(
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'🔄 Тип: Суточный\n'
f'💰 Цена: {format_price_kopeks(daily_price)}/день'
f'{discount_text}\n\n'
@@ -991,7 +992,7 @@ async def handle_custom_confirm(
await callback.message.edit_text(
f'🎉 <b>Подписка успешно оформлена!</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic_display}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {format_period(custom_days)}\n'
@@ -1115,7 +1116,7 @@ async def select_tariff_period(
await callback.message.edit_text(
f'✅ <b>Подтверждение покупки</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {format_period(period)}\n'
@@ -1150,7 +1151,7 @@ async def select_tariff_period(
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📅 Период: {format_period(period)}\n'
f'💰 Стоимость: {format_price_kopeks(final_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -1364,7 +1365,7 @@ async def confirm_tariff_purchase(
await callback.message.edit_text(
f'🎉 <b>Подписка успешно оформлена!</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {format_period(period)}\n'
@@ -1597,7 +1598,7 @@ async def confirm_daily_tariff_purchase(
await callback.message.edit_text(
f'🎉 <b>Суточная подписка оформлена!</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'🔄 Тип: Суточный\n'
@@ -1738,7 +1739,7 @@ async def show_tariff_extend(
await callback.message.edit_text(
f'🔄 <b>Продление подписки</b>{discount_hint}\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {actual_device_limit}\n\n'
'Выберите период продления:',
@@ -1805,7 +1806,7 @@ async def select_tariff_extend_period(
await callback.message.edit_text(
f'✅ <b>Подтверждение продления</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {actual_device_limit}\n'
f'📅 Период: {format_period(period)}\n'
@@ -1840,7 +1841,7 @@ async def select_tariff_extend_period(
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📅 Период: {format_period(period)}\n'
f'💰 К оплате: {format_price_kopeks(final_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -1982,7 +1983,7 @@ async def confirm_tariff_extend(
await callback.message.edit_text(
f'🎉 <b>Подписка успешно продлена!</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {actual_device_limit}\n'
f'📅 Добавлено: {format_period(period)}\n'
@@ -2059,10 +2060,10 @@ def format_tariff_switch_list_text(
discount_icon = '🔥'
price_text = f'от {format_price_kopeks(min_price, compact=True)}{discount_icon}'
lines.append(f'<b>{tariff.name}</b> — {traffic} / {tariff.device_limit} 📱 {price_text}')
lines.append(f'<b>{html.escape(tariff.name)}</b> — {traffic} / {tariff.device_limit} 📱 {price_text}')
if tariff.description:
lines.append(f'<i>{tariff.description}</i>')
lines.append(f'<i>{html.escape(tariff.description)}</i>')
lines.append('')
@@ -2198,7 +2199,7 @@ async def show_tariff_switch_list(
if current_tariff_id:
current_tariff = await get_tariff_by_id(db, current_tariff_id)
if current_tariff:
current_tariff_name = current_tariff.name
current_tariff_name = html.escape(current_tariff.name)
# Проверяем есть ли у пользователя скидки по периодам
promo_group = db_user.get_primary_promo_group() if hasattr(db_user, 'get_primary_promo_group') else None
@@ -2269,7 +2270,7 @@ async def select_tariff_switch(
if user_balance >= daily_price:
await callback.message.edit_text(
f'✅ <b>Подтверждение смены тарифа</b>\n\n'
f'📦 Новый тариф: <b>{tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'🔄 Тип: <b>Суточный</b>\n\n'
@@ -2295,7 +2296,7 @@ async def select_tariff_switch(
missing = daily_price - user_balance
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'🔄 Тип: Суточный\n'
f'💰 Цена: {format_price_kopeks(daily_price)}/день'
f'{discount_text}\n\n'
@@ -2312,7 +2313,7 @@ async def select_tariff_switch(
)
else:
# Для обычного тарифа показываем выбор периода
info_text = f"""📦 <b>{tariff.name}</b>
info_text = f"""📦 <b>{html.escape(tariff.name)}</b>
<b>Параметры нового тарифа:</b>
Трафик: {traffic}
@@ -2320,7 +2321,7 @@ async def select_tariff_switch(
"""
if tariff.description:
info_text += f'\n📝 {tariff.description}\n'
info_text += f'\n📝 {html.escape(tariff.description)}\n'
info_text += '\n⚠️ Оплачивается полная стоимость тарифа.\nВыберите период:'
@@ -2381,7 +2382,7 @@ async def select_tariff_switch_period(
if current_tariff_id:
current_tariff = await get_tariff_by_id(db, current_tariff_id)
if current_tariff:
current_tariff_name = current_tariff.name
current_tariff_name = html.escape(current_tariff.name)
# Получаем текущую подписку для расчёта оставшегося времени
subscription = await get_subscription_by_user_id(db, db_user.id)
@@ -2399,7 +2400,7 @@ async def select_tariff_switch_period(
await callback.message.edit_text(
f'✅ <b>Подтверждение переключения тарифа</b>\n\n'
f'📌 Текущий тариф: <b>{current_tariff_name}</b>\n'
f'📦 Новый тариф: <b>{tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'{time_info}\n'
@@ -2414,7 +2415,7 @@ async def select_tariff_switch_period(
missing = final_price - user_balance
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📅 Период: {format_period(period)}\n'
f'💰 К оплате: {format_price_kopeks(final_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -2591,7 +2592,7 @@ async def confirm_tariff_switch(
await callback.message.edit_text(
f'🎉 <b>Тариф успешно изменён!</b>\n\n'
f'📦 Новый тариф: <b>{tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'💰 Списано: {format_price_kopeks(final_price)}\n'
@@ -2788,7 +2789,7 @@ async def confirm_daily_tariff_switch(
await callback.message.edit_text(
f'🎉 <b>Тариф успешно изменён!</b>\n\n'
f'📦 Новый тариф: <b>{tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'🔄 Тип: Суточный\n'
@@ -2864,7 +2865,7 @@ def format_instant_switch_list_text(
"""Форматирует текст со списком тарифов для мгновенного переключения."""
lines = [
'📦 <b>Мгновенная смена тарифа</b>',
f'📌 Текущий: <b>{current_tariff.name}</b>',
f'📌 Текущий: <b>{html.escape(current_tariff.name)}</b>',
f'⏰ Осталось: <b>{remaining_days} дн.</b>',
'',
'💡 При переключении остаток дней сохраняется.',
@@ -2888,10 +2889,10 @@ def format_instant_switch_list_text(
else:
cost_text = '⬇️ Бесплатно'
lines.append(f'<b>{tariff.name}</b> — {traffic} / {tariff.device_limit} 📱 {cost_text}')
lines.append(f'<b>{html.escape(tariff.name)}</b> — {traffic} / {tariff.device_limit} 📱 {cost_text}')
if tariff.description:
lines.append(f'<i>{tariff.description}</i>')
lines.append(f'<i>{html.escape(tariff.description)}</i>')
lines.append('')
@@ -3109,10 +3110,10 @@ async def preview_instant_switch(
if user_balance >= daily_price:
await callback.message.edit_text(
f'🔄 <b>Переключение на суточный тариф</b>\n\n'
f'📌 Текущий: <b>{current_tariff.name}</b>\n'
f'📌 Текущий: <b>{html.escape(current_tariff.name)}</b>\n'
f' • Трафик: {current_traffic}\n'
f' • Устройств: {current_tariff.device_limit}\n\n'
f'📦 Новый: <b>{new_tariff.name}</b>\n'
f'📦 Новый: <b>{html.escape(new_tariff.name)}</b>\n'
f' • Трафик: {traffic}\n'
f' • Устройств: {new_tariff.device_limit}\n'
f' • Тип: 🔄 Суточный\n\n'
@@ -3128,7 +3129,7 @@ async def preview_instant_switch(
missing = daily_price - user_balance
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{new_tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(new_tariff.name)}</b>\n'
f'🔄 Тип: Суточный\n'
f'💰 Цена: {format_price_kopeks(daily_price)}/день'
f'{discount_text}\n\n'
@@ -3154,10 +3155,10 @@ async def preview_instant_switch(
if user_balance >= upgrade_cost:
await callback.message.edit_text(
f'⬆️ <b>Повышение тарифа</b>\n\n'
f'📌 Текущий: <b>{current_tariff.name}</b>\n'
f'📌 Текущий: <b>{html.escape(current_tariff.name)}</b>\n'
f' • Трафик: {current_traffic}\n'
f' • Устройств: {current_tariff.device_limit}\n\n'
f'📦 Новый: <b>{new_tariff.name}</b>\n'
f'📦 Новый: <b>{html.escape(new_tariff.name)}</b>\n'
f' • Трафик: {traffic}\n'
f' • Устройств: {new_tariff.device_limit}\n\n'
f'⏰ Осталось дней: <b>{remaining_days}</b>\n'
@@ -3171,7 +3172,7 @@ async def preview_instant_switch(
missing = upgrade_cost - user_balance
await callback.message.edit_text(
f'❌ <b>Недостаточно средств</b>\n\n'
f'📦 Новый тариф: <b>{new_tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(new_tariff.name)}</b>\n'
f'💰 Требуется доплата: {format_price_kopeks(upgrade_cost)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
f'⚠️ Не хватает: <b>{format_price_kopeks(missing)}</b>',
@@ -3182,10 +3183,10 @@ async def preview_instant_switch(
# Downgrade или тот же уровень - бесплатно
await callback.message.edit_text(
f'⬇️ <b>Переключение тарифа</b>\n\n'
f'📌 Текущий: <b>{current_tariff.name}</b>\n'
f'📌 Текущий: <b>{html.escape(current_tariff.name)}</b>\n'
f' • Трафик: {current_traffic}\n'
f' • Устройств: {current_tariff.device_limit}\n\n'
f'📦 Новый: <b>{new_tariff.name}</b>\n'
f'📦 Новый: <b>{html.escape(new_tariff.name)}</b>\n'
f' • Трафик: {traffic}\n'
f' • Устройств: {new_tariff.device_limit}\n\n'
f'⏰ Осталось дней: <b>{remaining_days}</b>\n'
@@ -3433,7 +3434,7 @@ async def confirm_instant_switch(
if is_new_daily:
await callback.message.edit_text(
f'🎉 <b>Тариф успешно изменён!</b>\n\n'
f'📦 Новый тариф: <b>{new_tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(new_tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {new_tariff.device_limit}\n'
f'🔄 Тип: Суточный\n'
@@ -3455,7 +3456,7 @@ async def confirm_instant_switch(
await callback.message.edit_text(
f'🎉 <b>Тариф успешно изменён!</b>\n\n'
f'📦 Новый тариф: <b>{new_tariff.name}</b>\n'
f'📦 Новый тариф: <b>{html.escape(new_tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {new_tariff.device_limit}\n'
f'⏰ Осталось дней: {remaining_days}\n'
@@ -3509,7 +3510,7 @@ async def return_to_saved_tariff_cart(
if cart_mode == 'daily_tariff_purchase':
await callback.message.edit_text(
f'❌ <b>Все еще недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'🔄 Тип: Суточный\n'
f'💰 Стоимость: {format_price_kopeks(total_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -3521,7 +3522,7 @@ async def return_to_saved_tariff_cart(
period = cart_data.get('period_days', 30)
await callback.message.edit_text(
f'❌ <b>Все еще недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📅 Период: {format_period(period)}\n'
f'💰 Стоимость: {format_price_kopeks(total_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -3533,7 +3534,7 @@ async def return_to_saved_tariff_cart(
period = cart_data.get('period_days', 30)
await callback.message.edit_text(
f'❌ <b>Все еще недостаточно средств</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📅 Период: {format_period(period)}\n'
f'💰 Стоимость: {format_price_kopeks(total_price)}\n\n'
f'💳 Ваш баланс: {format_price_kopeks(user_balance)}\n'
@@ -3552,7 +3553,7 @@ async def return_to_saved_tariff_cart(
await callback.message.edit_text(
f'✅ <b>Подтверждение покупки</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'🔄 Тип: Суточный\n'
@@ -3572,7 +3573,7 @@ async def return_to_saved_tariff_cart(
await callback.message.edit_text(
f'✅ <b>Подтверждение продления</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {format_period(period)}\n'
@@ -3602,7 +3603,7 @@ async def return_to_saved_tariff_cart(
await callback.message.edit_text(
f'✅ <b>Подтверждение покупки</b>\n\n'
f'📦 Тариф: <b>{tariff.name}</b>\n'
f'📦 Тариф: <b>{html.escape(tariff.name)}</b>\n'
f'📊 Трафик: {traffic}\n'
f'📱 Устройств: {tariff.device_limit}\n'
f'📅 Период: {format_period(period)}\n'
+15 -11
View File
@@ -1,4 +1,5 @@
import asyncio
import html
import time
import structlog
@@ -256,7 +257,7 @@ async def handle_ticket_message_input(message: types.Message, state: FSMContext,
texts = get_texts(db_user.language)
# Ограничим длину подтверждения чтобы не упереться в лимиты
safe_title = title if len(title) <= 200 else (title[:197] + '...')
safe_title = html.escape(title if len(title) <= 200 else (title[:197] + '...'))
creation_text = (
f'✅ <b>Тикет #{ticket.id} создан</b>\n\n'
f'📝 Заголовок: {safe_title}\n'
@@ -542,7 +543,7 @@ async def view_ticket(callback: types.CallbackQuery, db_user: User, db: AsyncSes
header = (
f'🎫 Тикет #{ticket.id}\n\n'
f'📝 Заголовок: {ticket.title}\n'
f'📝 Заголовок: {html.escape(ticket.title or "")}\n'
f'📊 Статус: {ticket.status_emoji} {status_text}\n'
f'📅 Создан: {format_local_datetime(ticket.created_at, "%d.%m.%Y %H:%M")}\n\n'
)
@@ -551,7 +552,7 @@ async def view_ticket(callback: types.CallbackQuery, db_user: User, db: AsyncSes
message_blocks.append(f'💬 Сообщения ({len(ticket.messages)}):\n\n')
for msg in ticket.messages:
sender = '👤 Вы' if msg.is_user_message else '🛠️ Поддержка'
block = f'{sender} ({format_local_datetime(msg.created_at, "%d.%m %H:%M")}):\n{msg.message_text}\n\n'
block = f'{sender} ({format_local_datetime(msg.created_at, "%d.%m %H:%M")}):\n{html.escape(msg.message_text or "")}\n\n'
if getattr(msg, 'has_media', False) and getattr(msg, 'media_type', None) == 'photo':
block += '📎 Вложение: фото\n\n'
message_blocks.append(block)
@@ -1006,9 +1007,9 @@ async def notify_admins_about_new_ticket(ticket: Ticket, db: AsyncSession):
user = await get_user_by_id(db, ticket.user_id)
except Exception:
user = None
full_name = user.full_name if user else 'Unknown'
full_name = html.escape(user.full_name or '') if user else 'Unknown'
telegram_id_display = (user.telegram_id or user.email or f'#{user.id}') if user else ''
username_display = (user.username or 'отсутствует') if user else 'отсутствует'
username_display = html.escape((user.username or 'отсутствует') if user else 'отсутствует')
# Загружаем первое сообщение для получения медиа и превью текста
first_message = await TicketMessageCRUD.get_first_message(db, ticket.id)
@@ -1022,17 +1023,19 @@ async def notify_admins_about_new_ticket(ticket: Ticket, db: AsyncSession):
if msg_text:
message_preview = msg_text[:200] + '...' if len(msg_text) > 200 else msg_text
safe_title = html.escape(title) if title else ''
notification_text = (
f'🎫 <b>НОВЫЙ ТИКЕТ</b>\n\n'
f'🆔 <b>ID:</b> <code>{ticket.id}</code>\n'
f'👤 <b>Пользователь:</b> {full_name}\n'
f'🆔 <b>ID:</b> <code>{telegram_id_display}</code>\n'
f'📱 <b>Username:</b> @{username_display}\n'
f'📝 <b>Заголовок:</b> {title or ""}\n'
f'📝 <b>Заголовок:</b> {safe_title}\n'
)
if message_preview:
notification_text += f'\n📩 <b>Сообщение:</b>\n{message_preview}\n'
notification_text += f'\n📩 <b>Сообщение:</b>\n{html.escape(message_preview)}\n'
notification_text += f'\n📅 <b>Создан:</b> {format_local_datetime(ticket.created_at, "%d.%m.%Y %H:%M")}\n'
@@ -1076,20 +1079,21 @@ async def notify_admins_about_ticket_reply(
user = await get_user_by_id(db, ticket.user_id)
except Exception:
user = None
full_name = user.full_name if user else 'Unknown'
full_name = html.escape(user.full_name or '') if user else 'Unknown'
telegram_id_display = (user.telegram_id or user.email or f'#{user.id}') if user else ''
username_display = (user.username or 'отсутствует') if user else 'отсутствует'
username_display = html.escape((user.username or 'отсутствует') if user else 'отсутствует')
reply_preview = reply_text[:200] + '...' if len(reply_text) > 200 else reply_text
safe_title = html.escape(title) if title else ''
notification_text = (
f'💬 <b>ОТВЕТ НА ТИКЕТ</b>\n\n'
f'🆔 <b>ID тикета:</b> <code>{ticket.id}</code>\n'
f'📝 <b>Заголовок:</b> {title or ""}\n'
f'📝 <b>Заголовок:</b> {safe_title}\n'
f'👤 <b>Пользователь:</b> {full_name}\n'
f'🆔 <b>ID:</b> <code>{telegram_id_display}</code>\n'
f'📱 <b>Username:</b> @{username_display}\n\n'
f'📩 <b>Сообщение:</b>\n{reply_preview}\n'
f'📩 <b>Сообщение:</b>\n{html.escape(reply_preview)}\n'
)
from app.services.maintenance_service import maintenance_service
+30 -28
View File
@@ -77,11 +77,11 @@ class AdminNotificationService:
return f'ID {referred_by_id} (не найден)'
if referrer.username:
return f'@{referrer.username} (ID: {referred_by_id})'
return f'@{html.escape(referrer.username)} (ID: {referred_by_id})'
if referrer.telegram_id:
return f'ID {referrer.telegram_id}'
if referrer.email:
return f'📧 {referrer.email}'
return f'📧 {html.escape(referrer.email)}'
return f'User#{referred_by_id}'
except Exception as e:
@@ -118,17 +118,17 @@ class AdminNotificationService:
def _get_user_display(self, user: User) -> str:
first_name = getattr(user, 'first_name', '') or ''
if first_name:
return first_name
return html.escape(first_name)
username = getattr(user, 'username', '') or ''
if username:
return username
return html.escape(username)
telegram_id = getattr(user, 'telegram_id', None)
if telegram_id is None:
email = getattr(user, 'email', None)
if email:
return email
return html.escape(email)
return f'User#{getattr(user, "id", "Unknown")}'
return f'ID{telegram_id}'
@@ -140,7 +140,7 @@ class AdminNotificationService:
email = getattr(user, 'email', None)
if email:
return f'📧 {email}'
return f'📧 {html.escape(email)}'
return f'User#{getattr(user, "id", "Unknown")}'
@@ -249,7 +249,7 @@ class AdminNotificationService:
if not promo_group:
return f'{icon} <b>{title}:</b> —'
lines = [f'{icon} <b>{title}:</b> {promo_group.name}']
lines = [f'{icon} <b>{title}:</b> {html.escape(promo_group.name)}']
discount_lines = self._format_promo_group_discounts(promo_group)
if discount_lines:
@@ -359,14 +359,14 @@ class AdminNotificationService:
'',
f'👤 <b>Пользователь:</b> {user_display}',
f'🆔 <b>{user_id_label}:</b> {user_id_display}',
f'📱 <b>Username:</b> @{getattr(user, "username", None) or "отсутствует"}',
f'📱 <b>Username:</b> @{html.escape(getattr(user, "username", None) or "отсутствует")}',
f'👥 <b>Статус:</b> {user_status}',
'',
]
# Промогруппа — только название, без скидок
if promo_group:
message_lines.append(f'🏷️ <b>Промогруппа:</b> {promo_group.name}')
message_lines.append(f'🏷️ <b>Промогруппа:</b> {html.escape(promo_group.name)}')
else:
message_lines.append('🏷️ <b>Промогруппа:</b> —')
@@ -419,7 +419,7 @@ class AdminNotificationService:
tariff = await get_tariff_by_id(db, subscription.tariff_id)
if tariff:
return tariff.name
return html.escape(tariff.name)
except Exception:
pass
return None
@@ -496,7 +496,7 @@ class AdminNotificationService:
# Добавляем username только если есть
username = getattr(user, 'username', None)
if username:
message_lines.append(f'📱 @{username}')
message_lines.append(f'📱 @{html.escape(username)}')
message_lines.append(f'📋 {user_status}')
@@ -659,13 +659,13 @@ class AdminNotificationService:
username = getattr(user, 'username', None)
if username:
message_lines.append(f'📱 @{username}')
message_lines.append(f'📱 @{html.escape(username)}')
message_lines.append(f'💳 {topup_status}')
# Промогруппа -- только название
if promo_group:
message_lines.append(f'🏷️ Промогруппа: {promo_group.name}')
message_lines.append(f'🏷️ Промогруппа: {html.escape(promo_group.name)}')
message_lines.append('')
@@ -699,7 +699,7 @@ class AdminNotificationService:
desc = transaction.description
if len(desc) > 120:
desc = desc[:117] + '...'
detail_lines.append(f'Описание: {desc}')
detail_lines.append(f'Описание: {html.escape(desc)}')
if transaction.created_at:
detail_lines.append(f'Создана: {format_local_datetime(transaction.created_at, "%d.%m.%Y %H:%M:%S")}')
@@ -918,7 +918,7 @@ class AdminNotificationService:
👤 <b>Пользователь:</b> {user_display}
🆔 <b>{user_id_label}:</b> {user_id_display}
📱 <b>Username:</b> @{getattr(user, 'username', None) or 'отсутствует'}
📱 <b>Username:</b> @{html.escape(getattr(user, 'username', None) or 'отсутствует')}
{promo_block}
@@ -1005,7 +1005,7 @@ class AdminNotificationService:
'',
f'👤 <b>Пользователь:</b> {user_display}',
f'🆔 <b>{user_id_label}:</b> {user_id_display}',
f'📱 <b>Username:</b> @{getattr(user, "username", None) or "отсутствует"}',
f'📱 <b>Username:</b> @{html.escape(getattr(user, "username", None) or "отсутствует")}',
'',
promo_block,
'',
@@ -1104,13 +1104,13 @@ class AdminNotificationService:
message_lines = [
'📣 <b>ПЕРЕХОД ПО РК</b>',
'',
f'🧾 {campaign.name} (<code>{campaign.start_parameter}</code>)',
f'🧾 {html.escape(campaign.name)} (<code>{html.escape(campaign.start_parameter)}</code>)',
'',
f'👤 {full_name} (<code>{telegram_user.id}</code>)',
f'👤 {html.escape(full_name)} (<code>{telegram_user.id}</code>)',
]
if telegram_user.username:
message_lines.append(f'📱 @{telegram_user.username}')
message_lines.append(f'📱 @{html.escape(telegram_user.username)}')
message_lines.append(f'📋 {user_status}')
@@ -1118,7 +1118,7 @@ class AdminNotificationService:
if user:
promo_group = await self._get_user_promo_group(db, user)
if promo_group:
message_lines.append(f'🏷️ Промогруппа: {promo_group.name}')
message_lines.append(f'🏷️ Промогруппа: {html.escape(promo_group.name)}')
message_lines.append('')
@@ -1130,7 +1130,7 @@ class AdminNotificationService:
tariff = await get_tariff_by_id(db, campaign.tariff_id)
if tariff:
tariff_name = tariff.name
tariff_name = html.escape(tariff.name)
except Exception:
pass
@@ -1196,7 +1196,9 @@ class AdminNotificationService:
title = '🤖 АВТОМАТИЧЕСКАЯ СМЕНА ПРОМОГРУППЫ' if automatic else '👥 СМЕНА ПРОМОГРУППЫ'
initiator_line = None
if initiator:
initiator_line = f'👮 <b>Инициатор:</b> {initiator.full_name} (ID: {initiator.telegram_id})'
initiator_line = (
f'👮 <b>Инициатор:</b> {html.escape(initiator.full_name)} (ID: {initiator.telegram_id})'
)
elif automatic:
initiator_line = '🤖 Автоматическое назначение'
user_display = self._get_user_display(user)
@@ -1208,7 +1210,7 @@ class AdminNotificationService:
'',
f'👤 <b>Пользователь:</b> {user_display}',
f'🆔 <b>{user_id_label}:</b> {user_id_display}',
f'📱 <b>Username:</b> @{getattr(user, "username", None) or "отсутствует"}',
f'📱 <b>Username:</b> @{html.escape(getattr(user, "username", None) or "отсутствует")}',
'',
self._format_promo_group_block(new_group, title='Новая промогруппа', icon='🏆'),
]
@@ -1653,7 +1655,7 @@ class AdminNotificationService:
elif status == 'maintenance':
if details.get('maintenance_reason'):
message_parts.append(f'🔧 <b>Причина:</b> {details["maintenance_reason"]}')
message_parts.append(f'🔧 <b>Причина:</b> {html.escape(details["maintenance_reason"])}')
if details.get('estimated_duration'):
message_parts.append(f'⏰ <b>Ожидаемая длительность:</b> {details["estimated_duration"]}')
@@ -1710,7 +1712,7 @@ class AdminNotificationService:
# Добавляем username только если есть
username = getattr(user, 'username', None)
if username:
message_lines.append(f'📱 @{username}')
message_lines.append(f'📱 @{html.escape(username)}')
# Тариф (если есть)
if tariff_name:
@@ -1816,7 +1818,7 @@ class AdminNotificationService:
username = getattr(user, 'username', None)
if username:
message_lines.append(f'📱 @{username}')
message_lines.append(f'📱 @{html.escape(username)}')
message_lines.append('')
@@ -1871,7 +1873,7 @@ class AdminNotificationService:
username = getattr(user, 'username', None)
if username:
message_lines.append(f'📱 @{username}')
message_lines.append(f'📱 @{html.escape(username)}')
message_lines.extend(
[
@@ -1916,7 +1918,7 @@ class AdminNotificationService:
message_lines = [
'🛑 <b>МАССОВАЯ БЛОКИРОВКА ПОЛЬЗОВАТЕЛЕЙ</b>',
'',
f'👮 <b>Администратор:</b> {admin_name}',
f'👮 <b>Администратор:</b> {html.escape(admin_name)}',
f'🆔 <b>ID администратора:</b> {admin_user_id}',
'',
'📊 <b>Результаты:</b>',
+7 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import html
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
@@ -1915,9 +1916,12 @@ class MonitoringService:
title = title[:57] + '...'
# Детали пользователя: имя, Telegram ID и username
full_name = ticket.user.full_name if ticket.user else 'Unknown'
full_name = html.escape(ticket.user.full_name or '') if ticket.user else 'Unknown'
telegram_id_display = ticket.user.telegram_id if ticket.user else ''
username_display = (ticket.user.username or 'отсутствует') if ticket.user else 'отсутствует'
username_display = html.escape(
(ticket.user.username or 'отсутствует') if ticket.user else 'отсутствует'
)
safe_title = html.escape(title) if title else ''
text = (
f'⏰ <b>Ожидание ответа на тикет превышено</b>\n\n'
@@ -1925,7 +1929,7 @@ class MonitoringService:
f'👤 <b>Пользователь:</b> {full_name}\n'
f'🆔 <b>Telegram ID:</b> <code>{telegram_id_display}</code>\n'
f'📱 <b>Username:</b> @{username_display}\n'
f'📝 <b>Заголовок:</b> {title or ""}\n'
f'📝 <b>Заголовок:</b> {safe_title}\n'
f'⏱️ <b>Ожидает ответа:</b> {waited_minutes} мин\n'
)
+3 -2
View File
@@ -1,4 +1,5 @@
import asyncio
import html
from collections.abc import Iterable
from types import SimpleNamespace
@@ -28,9 +29,9 @@ logger = structlog.get_logger(__name__)
def _build_poll_invitation_text(poll: Poll, language: str) -> str:
texts = get_texts(language)
lines: list[str] = [f'🗳️ <b>{poll.title}</b>']
lines: list[str] = [f'🗳️ <b>{html.escape(poll.title)}</b>']
if poll.description:
lines.append(poll.description)
lines.append(html.escape(poll.description))
if poll.reward_enabled and poll.reward_amount_kopeks > 0:
reward_line = texts.t(
+9 -8
View File
@@ -1,4 +1,5 @@
import asyncio
import html
from collections.abc import Sequence
from datetime import UTC, date, datetime, time, timedelta
from zoneinfo import ZoneInfo
@@ -281,7 +282,7 @@ class ReferralContestService:
lines = [
'🏆 <b>Конкурс рефералов</b>',
f'Название: <b>{contest.title}</b>',
f'Название: <b>{html.escape(contest.title)}</b>',
f'Статус: {"финал" if is_final else "дневная сводка"}',
f'Временная зона: <code>{tz.key}</code>',
f'Всего рефералов: <b>{total_events}</b>',
@@ -292,13 +293,13 @@ class ReferralContestService:
if leaderboard:
for idx, (name, score, _, is_virtual) in enumerate(leaderboard[:5], start=1):
virt_mark = ' 👻' if is_virtual else ''
lines.append(f'{idx}. {name}{virt_mark}{score}')
lines.append(f'{idx}. {html.escape(name)}{virt_mark}{score}')
else:
lines.append('Пока нет участников.')
if contest.prize_text:
lines.append('')
lines.append(f'Приз: {contest.prize_text}')
lines.append(f'Приз: {html.escape(contest.prize_text)}')
try:
await self.bot.send_message(
@@ -330,7 +331,7 @@ class ReferralContestService:
return
lines = [
f'🏆 {contest.title}',
f'🏆 {html.escape(contest.title)}',
'🏁 Итоги конкурса' if is_final else '📊 Промежуточные итоги',
f'Время зоны: {tz.key}',
f'Всего участников: <b>{len(leaderboard)}</b>',
@@ -340,13 +341,13 @@ class ReferralContestService:
if leaderboard:
for idx, (name, score, _, _is_virtual) in enumerate(leaderboard[:5], start=1):
lines.append(f'{idx}. {name}{score}')
lines.append(f'{idx}. {html.escape(name)}{score}')
else:
lines.append('Пока нет участников.')
if contest.prize_text:
lines.append('')
lines.append(f'Приз: {contest.prize_text}')
lines.append(f'Приз: {html.escape(contest.prize_text)}')
try:
await self.bot.send_message(
@@ -372,7 +373,7 @@ class ReferralContestService:
) -> str:
status_line = '🏁 Итоги конкурса' if is_final else '📊 Промежуточные итоги'
lines = [
f'🏆 {contest.title}',
f'🏆 {html.escape(contest.title)}',
status_line,
'',
f'Ваше место: <b>{rank}</b>',
@@ -383,7 +384,7 @@ class ReferralContestService:
if contest.prize_text:
lines.append('')
lines.append(f'Призовой фонд: {contest.prize_text}')
lines.append(f'Призовой фонд: {html.escape(contest.prize_text)}')
if not is_final:
remaining = contest.end_at - datetime.now(UTC)
+8 -6
View File
@@ -1,3 +1,5 @@
import html
import structlog
from aiogram import Bot
from sqlalchemy import delete
@@ -116,7 +118,7 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe
commission_percent = get_effective_referral_commission_percent(referrer)
referral_notification = (
f'🎉 <b>Добро пожаловать!</b>\n\n'
f'Вы перешли по реферальной ссылке пользователя <b>{referrer.full_name}</b>!'
f'Вы перешли по реферальной ссылке пользователя <b>{html.escape(referrer.full_name)}</b>!'
)
if settings.REFERRAL_FIRST_TOPUP_BONUS_KOPEKS > 0:
referral_notification += (
@@ -127,7 +129,7 @@ async def process_referral_registration(db: AsyncSession, new_user_id: int, refe
inviter_notification = (
f'👥 <b>Новый реферал!</b>\n\n'
f'По вашей ссылке зарегистрировался пользователь <b>{new_user.full_name}</b>!\n\n'
f'По вашей ссылке зарегистрировался пользователь <b>{html.escape(new_user.full_name)}</b>!\n\n'
f'💰 Когда он пополнит баланс от {settings.format_price(settings.REFERRAL_MINIMUM_TOPUP_KOPEKS)}, '
)
if settings.REFERRAL_INVITER_BONUS_KOPEKS > 0:
@@ -227,7 +229,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'Ваш реферал <b>{html.escape(user.full_name)}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
@@ -344,7 +346,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
bonus_breakdown = ' + '.join(bonus_parts)
inviter_bonus_notification = (
f'💰 <b>Реферальная награда!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> сделал первое пополнение '
f'Ваш реферал <b>{html.escape(user.full_name)}</b> сделал первое пополнение '
f'на {settings.format_price(topup_amount_kopeks)}!\n\n'
f'🎁 Ваша награда: {settings.format_price(inviter_bonus)}'
f' ({bonus_breakdown})\n\n'
@@ -398,7 +400,7 @@ async def process_referral_topup(db: AsyncSession, user_id: int, topup_amount_ko
if bot:
commission_notification = (
f'💰 <b>Реферальная комиссия!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> пополнил баланс на '
f'Ваш реферал <b>{html.escape(user.full_name)}</b> пополнил баланс на '
f'{settings.format_price(topup_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
@@ -479,7 +481,7 @@ async def process_referral_purchase(
if bot:
purchase_commission_notification = (
f'💰 <b>Комиссия с покупки!</b>\n\n'
f'Ваш реферал <b>{user.full_name}</b> совершил покупку на '
f'Ваш реферал <b>{html.escape(user.full_name)}</b> совершил покупку на '
f'{settings.format_price(purchase_amount_kopeks)}\n\n'
f'🎁 Ваша комиссия ({commission_percent}%): '
f'{settings.format_price(commission_amount)}\n\n'
+2 -1
View File
@@ -3,6 +3,7 @@
с анализом на подозрительную активность (отмывание денег).
"""
import html
import json
from datetime import UTC, datetime, timedelta
@@ -656,7 +657,7 @@ class ReferralWithdrawalService:
if details.get('suspicious_referrals'):
text += '\n🚨 <b>Подозрительные рефералы:</b>\n'
for sr in details['suspicious_referrals'][:5]:
text += f'{sr["name"]}: {sr["deposits_count"]} поп., {sr["deposits_total"] / 100:.0f}\n'
text += f'{html.escape(sr["name"])}: {sr["deposits_count"]} поп., {sr["deposits_total"] / 100:.0f}\n'
text += f' Флаги: {", ".join(sr["flags"])}\n'
# Источники дохода
@@ -2,6 +2,7 @@
from __future__ import annotations
import html
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
@@ -1157,7 +1158,7 @@ async def _auto_purchase_daily_tariff(
texts = get_texts(getattr(user, 'language', 'ru'))
message = (
f'✅ <b>Суточный тариф «{tariff.name}» активирован!</b>\n\n'
f'✅ <b>Суточный тариф «{html.escape(tariff.name)}» активирован!</b>\n\n'
f'💰 Списано: {final_price / 100:.0f} ₽ за первый день\n'
f'🔄 Средства будут списываться автоматически раз в сутки.\n\n'
f'ℹ️ Вы можете приостановить подписку в любой момент.'
@@ -2426,7 +2427,7 @@ async def try_resume_disabled_daily_after_topup(
'💳 Списано: {amount}\n'
'💰 Остаток: {balance}',
).format(
tariff_name=tariff.name,
tariff_name=html.escape(tariff.name),
amount=settings.format_price(daily_price),
balance=settings.format_price(user.balance_kopeks),
)
+3 -4
View File
@@ -4,6 +4,7 @@
"""
import asyncio
import html
from dataclasses import dataclass
from datetime import UTC, datetime, time, timedelta
@@ -694,11 +695,9 @@ class TrafficMonitoringServiceV2:
db_user = await get_user_by_remnawave_uuid(db, violation.user_uuid)
if db_user:
user_id_display = db_user.telegram_id or db_user.email or f'#{db_user.id}'
user_info = (
f'👤 <b>{db_user.full_name or "Без имени"}</b>\n🆔 ID: <code>{user_id_display}</code>\n'
)
user_info = f'👤 <b>{html.escape(db_user.full_name or "Без имени")}</b>\n🆔 ID: <code>{user_id_display}</code>\n'
if db_user.username:
user_info += f'📱 Username: @{db_user.username}\n'
user_info += f'📱 Username: @{html.escape(db_user.username)}\n'
if violation.check_type == 'fast':
check_type_emoji = ''
+15
View File
@@ -1,5 +1,20 @@
"""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:
"""Форматирует трафик."""