diff --git a/app/database/crud/server_squad.py b/app/database/crud/server_squad.py index 66ebc177..cc7ec4a1 100644 --- a/app/database/crud/server_squad.py +++ b/app/database/crud/server_squad.py @@ -306,9 +306,8 @@ async def sync_with_remnawave(db: AsyncSession, remnawave_squads: list[dict]) -> await create_server_squad( db=db, squad_uuid=squad_uuid, - display_name=_generate_display_name(original_name), + display_name=original_name, original_name=original_name, - country_code=_extract_country_code(original_name), price_kopeks=1000, is_available=False, ) @@ -482,219 +481,6 @@ async def get_random_trial_squad_uuid( return None -def _generate_display_name(original_name: str) -> str: - """Генерирует отображаемое название сервера на основе оригинального имени.""" - - country_names = { - # Европа - 'NL': '🇳🇱 Нидерланды', - 'DE': '🇩🇪 Германия', - 'FR': '🇫🇷 Франция', - 'GB': '🇬🇧 Великобритания', - 'UK': '🇬🇧 Великобритания', - 'IT': '🇮🇹 Италия', - 'ES': '🇪🇸 Испания', - 'PT': '🇵🇹 Португалия', - 'PL': '🇵🇱 Польша', - 'CZ': '🇨🇿 Чехия', - 'AT': '🇦🇹 Австрия', - 'CH': '🇨🇭 Швейцария', - 'SE': '🇸🇪 Швеция', - 'NO': '🇳🇴 Норвегия', - 'FI': '🇫🇮 Финляндия', - 'DK': '🇩🇰 Дания', - 'BE': '🇧🇪 Бельгия', - 'IE': '🇮🇪 Ирландия', - 'RO': '🇷🇴 Румыния', - 'BG': '🇧🇬 Болгария', - 'HU': '🇭🇺 Венгрия', - 'GR': '🇬🇷 Греция', - 'LV': '🇱🇻 Латвия', - 'LT': '🇱🇹 Литва', - 'EE': '🇪🇪 Эстония', - 'SK': '🇸🇰 Словакия', - 'SI': '🇸🇮 Словения', - 'HR': '🇭🇷 Хорватия', - 'RS': '🇷🇸 Сербия', - 'UA': '🇺🇦 Украина', - 'MD': '🇲🇩 Молдова', - 'BY': '🇧🇾 Беларусь', - 'LU': '🇱🇺 Люксембург', - # СНГ и Азия - 'RU': '🇷🇺 Россия', - 'KZ': '🇰🇿 Казахстан', - 'UZ': '🇺🇿 Узбекистан', - 'GE': '🇬🇪 Грузия', - 'AM': '🇦🇲 Армения', - 'AZ': '🇦🇿 Азербайджан', - # Америка - 'US': '🇺🇸 США', - 'CA': '🇨🇦 Канада', - 'MX': '🇲🇽 Мексика', - 'BR': '🇧🇷 Бразилия', - 'AR': '🇦🇷 Аргентина', - 'CL': '🇨🇱 Чили', - 'CO': '🇨🇴 Колумбия', - # Азия - 'JP': '🇯🇵 Япония', - 'KR': '🇰🇷 Южная Корея', - 'CN': '🇨🇳 Китай', - 'HK': '🇭🇰 Гонконг', - 'TW': '🇹🇼 Тайвань', - 'SG': '🇸🇬 Сингапур', - 'TH': '🇹🇭 Таиланд', - 'VN': '🇻🇳 Вьетнам', - 'MY': '🇲🇾 Малайзия', - 'ID': '🇮🇩 Индонезия', - 'PH': '🇵🇭 Филиппины', - 'IN': '🇮🇳 Индия', - 'PK': '🇵🇰 Пакистан', - # Ближний Восток - 'IL': '🇮🇱 Израиль', - 'TR': '🇹🇷 Турция', - 'AE': '🇦🇪 ОАЭ', - 'SA': '🇸🇦 Саудовская Аравия', - 'QA': '🇶🇦 Катар', - 'BH': '🇧🇭 Бахрейн', - 'KW': '🇰🇼 Кувейт', - # Океания - 'AU': '🇦🇺 Австралия', - 'NZ': '🇳🇿 Новая Зеландия', - # Африка - 'ZA': '🇿🇦 ЮАР', - 'EG': '🇪🇬 Египет', - 'NG': '🇳🇬 Нигерия', - 'KE': '🇰🇪 Кения', - } - - name_upper = original_name.upper() - - # Сначала ищем код как отдельный элемент (через - или _) - for code, display_name in country_names.items(): - if f'-{code}' in name_upper or f'_{code}' in name_upper: - return display_name - if name_upper.startswith(code + '-') or name_upper.startswith(code + '_'): - return display_name - if name_upper.endswith('-' + code) or name_upper.endswith('_' + code): - return display_name - if name_upper == code: - return display_name - - # Потом ищем просто вхождение кода - for code, display_name in country_names.items(): - if code in name_upper: - return display_name - - return f'🌍 {original_name}' - - -def _extract_country_code(original_name: str) -> str | None: - """Извлекает код страны из оригинального названия.""" - - # Полный список кодов стран - codes = [ - # Европа - 'NL', - 'DE', - 'FR', - 'GB', - 'UK', - 'IT', - 'ES', - 'PT', - 'PL', - 'CZ', - 'AT', - 'CH', - 'SE', - 'NO', - 'FI', - 'DK', - 'BE', - 'IE', - 'RO', - 'BG', - 'HU', - 'GR', - 'LV', - 'LT', - 'EE', - 'SK', - 'SI', - 'HR', - 'RS', - 'UA', - 'MD', - 'BY', - 'LU', - # СНГ - 'RU', - 'KZ', - 'UZ', - 'GE', - 'AM', - 'AZ', - # Америка - 'US', - 'CA', - 'MX', - 'BR', - 'AR', - 'CL', - 'CO', - # Азия - 'JP', - 'KR', - 'CN', - 'HK', - 'TW', - 'SG', - 'TH', - 'VN', - 'MY', - 'ID', - 'PH', - 'IN', - 'PK', - # Ближний Восток - 'IL', - 'TR', - 'AE', - 'SA', - 'QA', - 'BH', - 'KW', - # Океания - 'AU', - 'NZ', - # Африка - 'ZA', - 'EG', - 'NG', - 'KE', - ] - - name_upper = original_name.upper() - - # Сначала ищем код как отдельный элемент - for code in codes: - if f'-{code}' in name_upper or f'_{code}' in name_upper: - return code - if name_upper.startswith(code + '-') or name_upper.startswith(code + '_'): - return code - if name_upper.endswith('-' + code) or name_upper.endswith('_' + code): - return code - if name_upper == code: - return code - - # Потом просто ищем вхождение - for code in codes: - if code in name_upper: - return code - - return None - - async def get_server_statistics(db: AsyncSession) -> dict: total_result = await db.execute(select(func.count(ServerSquad.id))) total_servers = total_result.scalar() diff --git a/app/handlers/admin/promo_offers.py b/app/handlers/admin/promo_offers.py index 3a38dfb2..e614af0e 100644 --- a/app/handlers/admin/promo_offers.py +++ b/app/handlers/admin/promo_offers.py @@ -977,7 +977,7 @@ async def _render_squad_selection( if not selected_server: selected_server = await get_server_squad_by_uuid(db, selected_uuid) if selected_server: - selected_server_name = selected_server.display_name + selected_server_name = html.escape(selected_server.display_name) header = texts.t('ADMIN_PROMO_OFFER_SELECT_SQUAD_TITLE', '🌍 Выберите сквад') if selected_server_name: diff --git a/app/handlers/admin/remnawave.py b/app/handlers/admin/remnawave.py index 26fb9226..d05a4283 100644 --- a/app/handlers/admin/remnawave.py +++ b/app/handlers/admin/remnawave.py @@ -1,3 +1,4 @@ +import html import math from datetime import UTC, datetime, timedelta from typing import Any @@ -175,7 +176,7 @@ def _format_migration_server_label(texts, server) -> str: return texts.t( 'ADMIN_SQUAD_MIGRATION_SERVER_LABEL', '{name} — 👥 {users} ({status})', - ).format(name=server.display_name, users=server.current_users, status=status) + ).format(name=html.escape(server.display_name), users=server.current_users, status=status) def _build_migration_keyboard( diff --git a/app/handlers/admin/servers.py b/app/handlers/admin/servers.py index 148b4e0d..e19e3ad1 100644 --- a/app/handlers/admin/servers.py +++ b/app/handlers/admin/servers.py @@ -44,8 +44,8 @@ def _build_server_edit_view(server): Информация: • ID: {server.id} • UUID: {server.squad_uuid} -• Название: {server.display_name} -• Оригинальное: {server.original_name or 'Не указано'} +• Название: {html.escape(server.display_name)} +• Оригинальное: {html.escape(server.original_name) if server.original_name else 'Не указано'} • Статус: {status_emoji} Настройки: @@ -172,7 +172,7 @@ async def show_servers_list(callback: types.CallbackQuery, db_user: User, db: As status_emoji = '✅' if server.is_available else '❌' price_text = f'{int(server.price_rubles)} ₽' if server.price_kopeks > 0 else 'Бесплатно' - text += f'{i}. {status_emoji} {server.display_name}\n' + text += f'{i}. {status_emoji} {html.escape(server.display_name)}\n' text += f' 💰 Цена: {price_text}' if server.max_users: @@ -559,7 +559,7 @@ async def start_server_edit_name(callback: types.CallbackQuery, state: FSMContex await callback.message.edit_text( f'✏️ Редактирование названия\n\n' - f'Текущее название: {server.display_name}\n\n' + f'Текущее название: {html.escape(server.display_name)}\n\n' f'Отправьте новое название для сервера:', reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[ @@ -621,7 +621,7 @@ async def delete_server_confirm(callback: types.CallbackQuery, db_user: User, db 🗑️ Удаление сервера Вы действительно хотите удалить сервер: -{server.display_name} +{html.escape(server.display_name)} ⚠️ Внимание! Сервер можно удалить только если к нему нет активных подключений. @@ -658,7 +658,7 @@ async def delete_server_execute(callback: types.CallbackQuery, db_user: User, db await cache.delete_pattern('available_countries*') await callback.message.edit_text( - f'✅ Сервер {server.display_name} успешно удален!', + f'✅ Сервер {html.escape(server.display_name)} успешно удален!', reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[ [types.InlineKeyboardButton(text='📋 К списку серверов', callback_data='admin_servers_list')] @@ -668,7 +668,7 @@ async def delete_server_execute(callback: types.CallbackQuery, db_user: User, db ) else: await callback.message.edit_text( - f'❌ Не удалось удалить сервер {server.display_name}\n\nВозможно, к нему есть активные подключения.', + f'❌ Не удалось удалить сервер {html.escape(server.display_name)}\n\nВозможно, к нему есть активные подключения.', reply_markup=types.InlineKeyboardMarkup( inline_keyboard=[ [types.InlineKeyboardButton(text='🔙 К серверу', callback_data=f'admin_server_edit_{server_id}')] @@ -706,7 +706,7 @@ async def show_server_detailed_stats(callback: types.CallbackQuery, db_user: Use for i, server in enumerate(sorted_servers[:5], 1): price_text = f'{int(server.price_rubles)} ₽' if server.price_kopeks > 0 else 'Бесплатно' - text += f'{i}. {server.display_name} - {price_text}\n' + text += f'{i}. {html.escape(server.display_name)} - {price_text}\n' if not sorted_servers: text += 'Нет доступных серверов\n' @@ -968,7 +968,7 @@ async def start_server_edit_promo_groups( text = ( '🎯 Настройка промогрупп\n\n' - f'Сервер: {server.display_name}\n\n' + f'Сервер: {html.escape(server.display_name)}\n\n' 'Выберите промогруппы, которым будет доступен этот сервер.\n' 'Должна быть выбрана минимум одна промогруппа.' ) diff --git a/app/handlers/admin/users.py b/app/handlers/admin/users.py index 1e892921..7b097366 100644 --- a/app/handlers/admin/users.py +++ b/app/handlers/admin/users.py @@ -1,3 +1,4 @@ +import html import re from dataclasses import dataclass from datetime import UTC, datetime, timedelta @@ -868,7 +869,7 @@ async def _render_user_subscription_overview(callback: types.CallbackQuery, db: try: server = await get_server_squad_by_uuid(db, squad_uuid) if server: - text += f'• {server.display_name}\n' + text += f'• {html.escape(server.display_name)}\n' else: text += f'• {squad_uuid[:8]}... (неизвестный)\n' except Exception as e: diff --git a/app/handlers/balance/main.py b/app/handlers/balance/main.py index bb5dc343..18663d40 100644 --- a/app/handlers/balance/main.py +++ b/app/handlers/balance/main.py @@ -404,10 +404,7 @@ async def handle_balance_history_pagination(callback: types.CallbackQuery, db_us @error_handler async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext): from app.config import settings - from app.database.crud.subscription import get_subscription_by_user_id - from app.services.subscription_service import SubscriptionService from app.utils.payment_utils import get_payment_methods_text - from app.utils.pricing_utils import apply_percentage_discount, calculate_months_from_days texts = get_texts(db_user.language) @@ -430,139 +427,7 @@ async def show_payment_methods(callback: types.CallbackQuery, db_user: User, db: payment_text = get_payment_methods_text(db_user.language) - # Добавляем информацию о текущем тарифе пользователя - subscription = await get_subscription_by_user_id(db, db_user.id) - tariff_info = '' - if subscription and not subscription.is_trial: - # Рассчитываем приблизительную стоимость продления на 30 дней - duration_days = 30 # Берем для примера 30 дней - current_traffic = subscription.traffic_limit_gb - current_connected_squads = subscription.connected_squads or [] - current_device_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT - - try: - # Получаем цены для текущих параметров - from app.config import PERIOD_PRICES - from app.database.crud.tariff import get_tariff_by_id - - # В режиме тарифов берём цену из тарифа пользователя - tariff = None - tariff_price_found = False - base_price_original = 0 - if settings.is_tariffs_mode() and subscription.tariff_id: - tariff = await get_tariff_by_id(db, subscription.tariff_id) - if tariff and tariff.period_prices: - base_price_original = tariff.period_prices.get(str(duration_days), 0) - if base_price_original > 0: - tariff_price_found = True - - # Если не нашли в тарифе - используем PERIOD_PRICES - if base_price_original <= 0: - base_price_original = PERIOD_PRICES.get(duration_days, 0) - - if tariff_price_found: - # Тарифный режим: серверы и трафик включены в цену тарифа. - # Порядок: база + устройства → скидка на полную сумму (как в calculate_renewal_price). - from app.utils.promo_offer import get_user_active_promo_discount_percent - - original_price = base_price_original - - tariff_device_limit = tariff.device_limit if tariff.device_limit is not None else 0 - device_limit = ( - subscription.device_limit if subscription.device_limit is not None else tariff_device_limit - ) - extra_devices = max(0, device_limit - tariff_device_limit) - device_price_per_unit = ( - tariff.device_price_kopeks - if tariff and tariff.device_price_kopeks is not None - else settings.PRICE_PER_DEVICE - ) - months_in_period = calculate_months_from_days(duration_days) - devices_price = extra_devices * device_price_per_unit * months_in_period - original_price += devices_price - - # Скидка промогруппы на полную сумму (база + устройства) - period_discount_percent = db_user.get_promo_discount('period', duration_days) - discount_total = original_price * period_discount_percent // 100 - total_price = original_price - discount_total - - # Promo-offer скидка (временная) - promo_offer_percent = get_user_active_promo_discount_percent(db_user) - if promo_offer_percent > 0: - promo_offer_discount = total_price * promo_offer_percent // 100 - total_price = total_price - promo_offer_discount - else: - # Классический режим: серверы + трафик + устройства считаются отдельно - period_discount_percent = db_user.get_promo_discount('period', duration_days) - base_price, base_discount_total = apply_percentage_discount( - base_price_original, - period_discount_percent, - ) - from app.services.subscription_service import SubscriptionService - - subscription_service = SubscriptionService() - ( - servers_price_per_month, - per_server_monthly_prices, - ) = await subscription_service.get_countries_price_by_uuids( - current_connected_squads, - db, - promo_group_id=db_user.promo_group_id, - ) - servers_discount_percent = db_user.get_promo_discount('servers', duration_days) - total_servers_price = 0 - for server_price in per_server_monthly_prices: - discounted_per_month, discount_per_month = apply_percentage_discount( - server_price, - servers_discount_percent, - ) - total_servers_price += discounted_per_month - - traffic_price_per_month = settings.get_traffic_price(current_traffic) - traffic_discount_percent = db_user.get_promo_discount('traffic', duration_days) - traffic_discounted_per_month, traffic_discount_per_month = apply_percentage_discount( - traffic_price_per_month, - traffic_discount_percent, - ) - - additional_devices = max(0, (current_device_limit or 0) - settings.DEFAULT_DEVICE_LIMIT) - devices_price_per_month = additional_devices * settings.PRICE_PER_DEVICE - devices_discount_percent = db_user.get_promo_discount('devices', duration_days) - devices_discounted_per_month, devices_discount_per_month = apply_percentage_discount( - devices_price_per_month, - devices_discount_percent, - ) - - months_in_period = calculate_months_from_days(duration_days) - total_price = ( - base_price - + total_servers_price * months_in_period - + traffic_discounted_per_month * months_in_period - + devices_discounted_per_month * months_in_period - ) - - traffic_value = current_traffic or 0 - if traffic_value <= 0: - traffic_display = texts.t('TRAFFIC_UNLIMITED_SHORT', 'Безлимит') - else: - traffic_display = texts.format_traffic(traffic_value) - - current_tariff_desc = ( - f'📱 Подписка: {len(current_connected_squads)} серверов, ' - f'{traffic_display}, {current_device_limit} устр.' - ) - estimated_price_info = ( - f'💰 Стоимость продления (примерно): {texts.format_price(total_price)} за {duration_days} дней' - ) - - tariff_info = f'\n\n📋 Ваш текущий тариф:\n{current_tariff_desc}\n{estimated_price_info}' - except Exception as e: - logger.warning( - 'Не удалось рассчитать стоимость текущей подписки для пользователя', db_user_id=db_user.id, error=e - ) - tariff_info = '' - - full_text = payment_text + tariff_info + full_text = payment_text keyboard = get_payment_methods_keyboard(0, db_user.language) diff --git a/app/handlers/server_status.py b/app/handlers/server_status.py index a2e551f8..72f1b947 100644 --- a/app/handlers/server_status.py +++ b/app/handlers/server_status.py @@ -1,3 +1,4 @@ +import html from datetime import UTC, datetime import structlog @@ -189,7 +190,7 @@ def _format_server_lines( else: latency_text = texts.t('SERVER_STATUS_OFFLINE', 'нет ответа') - name = server.display_name or server.name + name = html.escape(server.display_name or server.name) flag_prefix = f'{server.flag} ' if server.flag else '' server_line = f'{flag_prefix}{name} — {latency_text}' lines.append(f'
{server_line}
') diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py index c7b6f42d..a0c8a066 100644 --- a/app/handlers/stars_payments.py +++ b/app/handlers/stars_payments.py @@ -301,7 +301,7 @@ async def _handle_guest_purchase_payment( from app.services.payment.common import try_fulfill_guest_purchase try: - purchase_token = payload[len('guest_purchase_'):] + purchase_token = payload[len('guest_purchase_') :] if not purchase_token or not _PURCHASE_TOKEN_RE.match(purchase_token): logger.error('Invalid purchase_token format in guest_purchase payload', payload=payload) await message.answer('❌ Ошибка: неверный формат платежа.') @@ -356,8 +356,7 @@ async def _handle_guest_purchase_payment( ) elif result is False: await message.answer( - '❌ Произошла ошибка при обработке подарочной подписки. ' - 'Обратитесь в поддержку.', + '❌ Произошла ошибка при обработке подарочной подписки. Обратитесь в поддержку.', ) else: logger.error('try_fulfill_guest_purchase returned None for Stars gift', payload=payload) diff --git a/app/handlers/subscription/countries.py b/app/handlers/subscription/countries.py index f611594d..088eed83 100644 --- a/app/handlers/subscription/countries.py +++ b/app/handlers/subscription/countries.py @@ -1,3 +1,4 @@ +import html from datetime import UTC, datetime from aiogram import types @@ -66,7 +67,7 @@ async def handle_add_countries(callback: types.CallbackQuery, db_user: User, db: current_countries_names = [] for country in countries: if country['uuid'] in current_countries: - current_countries_names.append(country['name']) + current_countries_names.append(html.escape(country['name'])) current_list = ( '\n'.join(f'• {name}' for name in current_countries_names) @@ -659,8 +660,8 @@ def _build_countries_selection_text(countries: list[dict], base_text: str) -> st continue desc = country.get('description', '').strip() if desc: - name = country.get('name', '') - descriptions.append(f'{name}\n{desc}') + name = html.escape(country.get('name', '')) + descriptions.append(f'{name}\n{html.escape(desc)}') if not descriptions: return base_text @@ -841,9 +842,9 @@ async def confirm_add_countries_to_subscription( total_price += charged_price total_discount_value += int(discount_per_month * charged_days / 30) - new_countries_names.append(country['name']) + new_countries_names.append(html.escape(country['name'])) if country['uuid'] in removed_countries: - removed_countries_names.append(country['name']) + removed_countries_names.append(html.escape(country['name'])) if new_countries and db_user.balance_kopeks < total_price: missing_kopeks = total_price - db_user.balance_kopeks diff --git a/app/handlers/subscription/devices.py b/app/handlers/subscription/devices.py index 5e84d36b..d5597c7b 100644 --- a/app/handlers/subscription/devices.py +++ b/app/handlers/subscription/devices.py @@ -82,7 +82,7 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str: for uuid in squad_uuids: server = await get_server_squad_by_uuid(db, uuid) if server: - server_names.append(server.display_name) + server_names.append(html_mod.escape(server.display_name)) logger.debug('Найден сервер в БД', uuid=uuid, display_name=server.display_name) else: logger.warning('Сервер с UUID не найден в БД', uuid=uuid) @@ -92,7 +92,7 @@ async def get_servers_display_names(squad_uuids: list[str]) -> str: for uuid in squad_uuids: for country in countries: if country['uuid'] == uuid: - server_names.append(country['name']) + server_names.append(html_mod.escape(country['name'])) logger.debug('Найден сервер в кэше', uuid=uuid, country=country['name']) break diff --git a/app/handlers/subscription/pricing.py b/app/handlers/subscription/pricing.py index bcbd8114..e5f7d667 100644 --- a/app/handlers/subscription/pricing.py +++ b/app/handlers/subscription/pricing.py @@ -1,3 +1,4 @@ +import html from datetime import UTC, datetime from typing import Any @@ -74,7 +75,7 @@ async def _prepare_subscription_summary( if country['uuid'] in selected_country_ids: server_price_per_month = country['price_kopeks'] countries_price_per_month += server_price_per_month - selected_countries_names.append(country['name']) + selected_countries_names.append(html.escape(country['name'])) server_monthly_prices.append(server_price_per_month) servers_discount_percent = db_user.get_promo_discount( diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 2ca22c35..1b7f7cf6 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -1,3 +1,4 @@ +import html from datetime import UTC, datetime, timedelta from typing import Any @@ -623,7 +624,7 @@ async def show_trial_offer(callback: types.CallbackQuery, db_user: User, db: Asy tariff_squads = await get_server_squads_by_uuids(db, trial_tariff.allowed_squads) if tariff_squads: if len(tariff_squads) == 1: - trial_server_name = tariff_squads[0].display_name + trial_server_name = html.escape(tariff_squads[0].display_name) else: trial_server_name = texts.t( 'TRIAL_SERVER_RANDOM_POOL', @@ -633,7 +634,7 @@ async def show_trial_offer(callback: types.CallbackQuery, db_user: User, db: Asy trial_squads = await get_trial_eligible_server_squads(db, include_unavailable=True) if trial_squads: if len(trial_squads) == 1: - trial_server_name = trial_squads[0].display_name + trial_server_name = html.escape(trial_squads[0].display_name) else: trial_server_name = texts.t( 'TRIAL_SERVER_RANDOM_POOL',