diff --git a/app/handlers/admin/user_messages.py b/app/handlers/admin/user_messages.py index 69209f0d..ceacd20a 100644 --- a/app/handlers/admin/user_messages.py +++ b/app/handlers/admin/user_messages.py @@ -14,7 +14,6 @@ from app.database.models import User from app.keyboards.admin import get_admin_main_keyboard from app.utils.validators import ( get_html_help_text, - format_telegram_quote, sanitize_html, validate_html_tags, ) @@ -183,14 +182,14 @@ async def process_new_message_text( ) await state.clear() - + await message.answer( f"✅ Сообщение добавлено!\n\n" f"ID: {new_message.id}\n" f"Статус: {'🟢 Активно' if new_message.is_active else '🔴 Неактивно'}\n" f"Создано: {new_message.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Предварительный просмотр:\n" - f"{format_telegram_quote(sanitize_html(message_text))}", + f"
{message_text}
", reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) @@ -319,7 +318,7 @@ async def view_user_message( await callback.answer("❌ Сообщение не найдено", show_alert=True) return - safe_content = format_telegram_quote(sanitize_html(message.message_text)) + safe_content = sanitize_html(message.message_text) status_text = "🟢 Активно" if message.is_active else "🔴 Неактивно" @@ -329,7 +328,7 @@ async def view_user_message( f"Создано: {message.created_at.strftime('%d.%m.%Y %H:%M')}\n" f"Обновлено: {message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Содержимое:\n" - f"{safe_content}" + f"
{safe_content}
" ) await callback.message.edit_text( @@ -459,7 +458,7 @@ async def edit_user_message_start( await callback.message.edit_text( f"✏️ Редактирование сообщения ID {message.id}\n\n" f"Текущий текст:\n" - f"{format_telegram_quote(sanitize_html(message.message_text))}\n\n" + f"
{sanitize_html(message.message_text)}
\n\n" f"Введите новый текст сообщения или отправьте /cancel для отмены:", parse_mode="HTML" ) @@ -524,7 +523,7 @@ async def process_edit_message_text( f"ID: {updated_message.id}\n" f"Обновлено: {updated_message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"Новый текст:\n" - f"{format_telegram_quote(sanitize_html(new_text))}", + f"
{sanitize_html(new_text)}
", reply_markup=get_user_messages_keyboard(db_user.language), parse_mode="HTML" ) diff --git a/app/handlers/server_status.py b/app/handlers/server_status.py index 38406026..c223dc5a 100644 --- a/app/handlers/server_status.py +++ b/app/handlers/server_status.py @@ -194,7 +194,7 @@ def _format_server_lines( name = 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}") + lines.append(f"
{server_line}
") return lines diff --git a/app/handlers/subscription/purchase.py b/app/handlers/subscription/purchase.py index 0d76956b..cba86cfa 100644 --- a/app/handlers/subscription/purchase.py +++ b/app/handlers/subscription/purchase.py @@ -355,7 +355,7 @@ async def show_subscription_info( if show_devices and devices_list: message += "\n\n" + texts.t( "SUBSCRIPTION_CONNECTED_DEVICES_TITLE", - "📱 Подключенные устройства:\n", + "
📱 Подключенные устройства:\n", ) for device in devices_list[:5]: platform = device.get('platform', 'Unknown') @@ -365,7 +365,7 @@ async def show_subscription_info( if len(device_info) > 35: device_info = device_info[:32] + "..." message += f"• {device_info}\n" - message += texts.t("SUBSCRIPTION_CONNECTED_DEVICES_FOOTER", "") + message += texts.t("SUBSCRIPTION_CONNECTED_DEVICES_FOOTER", "
") subscription_link = get_display_subscription_link(subscription) hide_subscription_link = settings.should_hide_subscription_link() diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json index 0cd59252..34067554 100644 --- a/app/localization/locales/en.json +++ b/app/localization/locales/en.json @@ -1276,8 +1276,8 @@ "SUBSCRIPTION_APPS_PROMPT": "Choose an app to connect:", "SUBSCRIPTION_APPS_TITLE": "📱 Apps for {device_name}", "SUBSCRIPTION_APP_NOT_FOUND": "❌ App not found", - "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", - "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 Connected devices:\n", + "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", + "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "
📱 Connected devices:\n", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 Connect subscription\n\n📱 Tap the button below to open the app:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 Connect subscription\n\n🔗 Subscription link:\n{subscription_url}\n\n💡 Choose your device to get detailed setup instructions:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 Connect subscription\n\nℹ️ The subscription link is available via the buttons below or in the “My subscription” section.\n\n💡 Choose your device to get detailed setup instructions:", diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json index 13770fbc..3d3aef2c 100644 --- a/app/localization/locales/ru.json +++ b/app/localization/locales/ru.json @@ -1288,8 +1288,8 @@ "SUBSCRIPTION_APPS_PROMPT": "Выберите приложение для подключения:", "SUBSCRIPTION_APPS_TITLE": "📱 Приложения для {device_name}", "SUBSCRIPTION_APP_NOT_FOUND": "❌ Приложение не найдено", - "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", - "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 Подключенные устройства:\n", + "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "
", + "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "
📱 Подключенные устройства:\n", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 Подключить подписку\n\n📱 Нажмите кнопку ниже, чтобы открыть приложение:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 Подключить подписку\n\n🔗 Ссылка подписки:\n{subscription_url}\n\n💡 Выберите ваше устройство для получения подробной инструкции по настройке:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 Подключить подписку\n\nℹ️ Ссылка подписки доступна по кнопкам ниже или в разделе «Моя подписка».\n\n💡 Выберите ваше устройство для получения подробной инструкции по настройке:", diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json index a9bce5be..d291f56a 100644 --- a/app/localization/locales/ua.json +++ b/app/localization/locales/ua.json @@ -1280,8 +1280,8 @@ "SUBSCRIPTION_APPS_PROMPT": "Оберіть додаток для підключення:", "SUBSCRIPTION_APPS_TITLE": "📱 Додатки для {device_name}", "SUBSCRIPTION_APP_NOT_FOUND": "❌ Додаток не знайдено", - "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", - "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 Підключені пристрої:\n", + "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "
", + "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "
📱 Підключені пристрої:\n", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 Підключити підписку\n\n📱 Натисніть кнопку нижче, щоб відкрити додаток:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 Підключити підписку\n\n🔗 Посилання підписки:\n{subscription_url}\n\n💡 Оберіть ваш пристрій для отримання детальної інструкції з налаштування:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 Підключити підписку\n\nℹ️ Посилання підписки доступне за кнопками нижче або в розділі «Моя підписка».\n\n💡 Оберіть ваш пристрій для отримання детальної інструкції з налаштування:", diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json index 3d892f37..d6c32b4e 100644 --- a/app/localization/locales/zh.json +++ b/app/localization/locales/zh.json @@ -1279,8 +1279,8 @@ "SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:", "SUBSCRIPTION_APPS_TITLE":"📱适用于{device_name}的应用程序", "SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序", -"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"", -"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"📱已连接设备:\n", +"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"
", +"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"
📱已连接设备:\n", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀连接订阅\n\n📱点击下方按钮打开应用程序:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱连接订阅\n\n🔗订阅链接:\n{subscription_url}\n\n💡请选择您的设备以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱连接订阅\n\nℹ️订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡请选择您的设备以获取详细设置说明:", @@ -1606,8 +1606,8 @@ "SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:", "SUBSCRIPTION_APPS_TITLE":"📱适用于{device_name}的应用程序", "SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序", -"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"", -"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"📱已连接设备:\n", +"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"
", +"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"
📱已连接设备:\n", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀连接订阅\n\n📱点击下方按钮打开应用程序:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱连接订阅\n\n🔗订阅链接:\n{subscription_url}\n\n💡请选择您的设备以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱连接订阅\n\nℹ️订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡请选择您的设备以获取详细设置说明:", diff --git a/app/utils/message_patch.py b/app/utils/message_patch.py index 7302f6ef..134ff1ea 100644 --- a/app/utils/message_patch.py +++ b/app/utils/message_patch.py @@ -1,5 +1,3 @@ -import html -import re from pathlib import Path from typing import Any, Dict @@ -65,14 +63,6 @@ def append_privacy_hint(text: str | None, language: str | None) -> str: return hint -def _strip_html(text: str | None) -> str: - if not text: - return "" - - plain_text = html.unescape(re.sub(r"<[^>]+>", "", text)) - return plain_text.strip() - - def prepare_privacy_safe_kwargs(kwargs: Dict[str, Any] | None = None) -> Dict[str, Any]: safe_kwargs: Dict[str, Any] = dict(kwargs or {}) safe_kwargs.pop("reply_markup", None) @@ -110,10 +100,7 @@ async def _answer_with_photo(self: Message, text: str = None, **kwargs): safe_kwargs = prepare_privacy_safe_kwargs(kwargs) return await _original_answer(self, fallback_text, **safe_kwargs) # Фоллбек, если Telegram ругается на caption или другое ограничение: отправим как текст - fallback_text = _strip_html(text) - safe_kwargs = dict(kwargs) - safe_kwargs.pop("parse_mode", None) - return await _original_answer(self, fallback_text, **safe_kwargs) + return await _original_answer(self, text, **kwargs) except Exception: return await _original_answer(self, text, **kwargs) return await _original_answer(self, text, **kwargs) @@ -166,10 +153,7 @@ async def _edit_with_photo(self: Message, text: str, **kwargs): await self.delete() except Exception: pass - fallback_text = _strip_html(text) - safe_kwargs = dict(kwargs) - safe_kwargs.pop("parse_mode", None) - return await _original_answer(self, fallback_text, **safe_kwargs) + return await _original_answer(self, text, **kwargs) return await _original_edit_text(self, text, **kwargs) @@ -178,3 +162,4 @@ def patch_message_methods(): return Message.answer = _answer_with_photo Message.edit_text = _edit_with_photo + diff --git a/app/utils/photo_message.py b/app/utils/photo_message.py index 95d04110..f7e65752 100644 --- a/app/utils/photo_message.py +++ b/app/utils/photo_message.py @@ -1,6 +1,3 @@ -import html -import re - from aiogram import types from aiogram.exceptions import TelegramBadRequest from aiogram.types import FSInputFile, InputMediaPhoto @@ -36,14 +33,6 @@ def _get_language(callback: types.CallbackQuery) -> str | None: return None -def _strip_html(text: str | None) -> str: - if not text: - return "" - - plain_text = html.unescape(re.sub(r"<[^>]+>", "", text)) - return plain_text.strip() - - def _build_base_kwargs(keyboard: types.InlineKeyboardMarkup | None, parse_mode: str | None): kwargs: dict[str, object] = {} if parse_mode is not None: @@ -68,19 +57,6 @@ async def _answer_text( kwargs = prepare_privacy_safe_kwargs(kwargs) kwargs.setdefault("parse_mode", parse_mode or "HTML") - try: - await callback.message.answer( - caption, - **kwargs, - ) - return - except TelegramBadRequest as send_error: - if is_privacy_restricted_error(send_error): - caption = append_privacy_hint(caption, language) - kwargs = prepare_privacy_safe_kwargs(kwargs) - else: - caption = _strip_html(caption) - kwargs.pop("parse_mode", None) await callback.message.answer( caption, diff --git a/app/utils/validators.py b/app/utils/validators.py index 0e3b7432..43cecbab 100644 --- a/app/utils/validators.py +++ b/app/utils/validators.py @@ -4,13 +4,14 @@ from datetime import datetime import html ALLOWED_HTML_TAGS = { - 'b', 'strong', - 'i', 'em', - 'u', 'ins', - 's', 'strike', 'del', - 'code', - 'pre', - 'a' + 'b', 'strong', + 'i', 'em', + 'u', 'ins', + 's', 'strike', 'del', + 'code', + 'pre', + 'a', + 'blockquote' } SELF_CLOSING_TAGS = { @@ -145,24 +146,6 @@ def sanitize_html(text: str) -> str: return text -def strip_blockquote_tags(text: str) -> str: - """Remove Telegram-unsupported blockquote tags (both raw and escaped).""" - if not text: - return text - - without_tags = re.sub(r"\s*", "", text, flags=re.IGNORECASE) - without_escaped = re.sub(r"</?blockquote[^&]*>\s*", "", without_tags, flags=re.IGNORECASE) - return without_escaped - - -def format_telegram_quote(text: str | None) -> str: - """Format text as a lightweight quote safe for Telegram HTML parse mode.""" - clean_text = strip_blockquote_tags(text or "").strip() - if not clean_text: - return "—" - return f"❝ {clean_text} ❞" - - def sanitize_telegram_name(name: Optional[str]) -> Optional[str]: """Санитизация Telegram-имени для безопасной вставки в HTML и хранения. Заменяет угловые скобки и амперсанд на безопасные визуальные аналоги.