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:
@@ -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,
|
||||
|
||||
@@ -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} записей'
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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')]]
|
||||
),
|
||||
|
||||
@@ -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('Пока нет виртуальных участников.')
|
||||
|
||||
|
||||
@@ -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'Теперь используются стандартные правила по умолчанию.'
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = 'Ошибка включения режима техработ'
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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')]]
|
||||
),
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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=[
|
||||
[
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
# Добавляем информацию о тарифе перед "Выберите действие"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>',
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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 = '⚡'
|
||||
|
||||
@@ -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:
|
||||
"""Форматирует трафик."""
|
||||
|
||||
Reference in New Issue
Block a user