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"?blockquote>\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 и хранения.
Заменяет угловые скобки и амперсанд на безопасные визуальные аналоги.