Revert "Remove blockquote markup to prevent Telegram parse errors"

This commit is contained in:
Egor
2025-11-28 06:02:08 +03:00
committed by GitHub
parent 4ae2234b7f
commit 150f9e741a
10 changed files with 30 additions and 87 deletions

View File

@@ -14,7 +14,6 @@ from app.database.models import User
from app.keyboards.admin import get_admin_main_keyboard from app.keyboards.admin import get_admin_main_keyboard
from app.utils.validators import ( from app.utils.validators import (
get_html_help_text, get_html_help_text,
format_telegram_quote,
sanitize_html, sanitize_html,
validate_html_tags, validate_html_tags,
) )
@@ -183,14 +182,14 @@ async def process_new_message_text(
) )
await state.clear() await state.clear()
await message.answer( await message.answer(
f"✅ <b>Сообщение добавлено!</b>\n\n" f"✅ <b>Сообщение добавлено!</b>\n\n"
f"<b>ID:</b> {new_message.id}\n" f"<b>ID:</b> {new_message.id}\n"
f"<b>Статус:</b> {'🟢 Активно' if new_message.is_active else '🔴 Неактивно'}\n" f"<b>Статус:</b> {'🟢 Активно' if new_message.is_active else '🔴 Неактивно'}\n"
f"<b>Создано:</b> {new_message.created_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"<b>Создано:</b> {new_message.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
f"<b>Предварительный просмотр:</b>\n" f"<b>Предварительный просмотр:</b>\n"
f"{format_telegram_quote(sanitize_html(message_text))}", f"<blockquote>{message_text}</blockquote>",
reply_markup=get_user_messages_keyboard(db_user.language), reply_markup=get_user_messages_keyboard(db_user.language),
parse_mode="HTML" parse_mode="HTML"
) )
@@ -319,7 +318,7 @@ async def view_user_message(
await callback.answer("❌ Сообщение не найдено", show_alert=True) await callback.answer("❌ Сообщение не найдено", show_alert=True)
return 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 "🔴 Неактивно" status_text = "🟢 Активно" if message.is_active else "🔴 Неактивно"
@@ -329,7 +328,7 @@ async def view_user_message(
f"<b>Создано:</b> {message.created_at.strftime('%d.%m.%Y %H:%M')}\n" f"<b>Создано:</b> {message.created_at.strftime('%d.%m.%Y %H:%M')}\n"
f"<b>Обновлено:</b> {message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"<b>Обновлено:</b> {message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n"
f"<b>Содержимое:</b>\n" f"<b>Содержимое:</b>\n"
f"{safe_content}" f"<blockquote>{safe_content}</blockquote>"
) )
await callback.message.edit_text( await callback.message.edit_text(
@@ -459,7 +458,7 @@ async def edit_user_message_start(
await callback.message.edit_text( await callback.message.edit_text(
f"✏️ <b>Редактирование сообщения ID {message.id}</b>\n\n" f"✏️ <b>Редактирование сообщения ID {message.id}</b>\n\n"
f"<b>Текущий текст:</b>\n" f"<b>Текущий текст:</b>\n"
f"{format_telegram_quote(sanitize_html(message.message_text))}\n\n" f"<blockquote>{sanitize_html(message.message_text)}</blockquote>\n\n"
f"Введите новый текст сообщения или отправьте /cancel для отмены:", f"Введите новый текст сообщения или отправьте /cancel для отмены:",
parse_mode="HTML" parse_mode="HTML"
) )
@@ -524,7 +523,7 @@ async def process_edit_message_text(
f"<b>ID:</b> {updated_message.id}\n" f"<b>ID:</b> {updated_message.id}\n"
f"<b>Обновлено:</b> {updated_message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n" f"<b>Обновлено:</b> {updated_message.updated_at.strftime('%d.%m.%Y %H:%M')}\n\n"
f"<b>Новый текст:</b>\n" f"<b>Новый текст:</b>\n"
f"{format_telegram_quote(sanitize_html(new_text))}", f"<blockquote>{sanitize_html(new_text)}</blockquote>",
reply_markup=get_user_messages_keyboard(db_user.language), reply_markup=get_user_messages_keyboard(db_user.language),
parse_mode="HTML" parse_mode="HTML"
) )

View File

@@ -194,7 +194,7 @@ def _format_server_lines(
name = server.display_name or server.name name = server.display_name or server.name
flag_prefix = f"{server.flag} " if server.flag else "" flag_prefix = f"{server.flag} " if server.flag else ""
server_line = f"{flag_prefix}{name}{latency_text}" server_line = f"{flag_prefix}{name}{latency_text}"
lines.append(f"{server_line}") lines.append(f"<blockquote>{server_line}</blockquote>")
return lines return lines

View File

@@ -355,7 +355,7 @@ async def show_subscription_info(
if show_devices and devices_list: if show_devices and devices_list:
message += "\n\n" + texts.t( message += "\n\n" + texts.t(
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE",
"📱 <b>Подключенные устройства:</b>\n", "<blockquote>📱 <b>Подключенные устройства:</b>\n",
) )
for device in devices_list[:5]: for device in devices_list[:5]:
platform = device.get('platform', 'Unknown') platform = device.get('platform', 'Unknown')
@@ -365,7 +365,7 @@ async def show_subscription_info(
if len(device_info) > 35: if len(device_info) > 35:
device_info = device_info[:32] + "..." device_info = device_info[:32] + "..."
message += f"{device_info}\n" message += f"{device_info}\n"
message += texts.t("SUBSCRIPTION_CONNECTED_DEVICES_FOOTER", "") message += texts.t("SUBSCRIPTION_CONNECTED_DEVICES_FOOTER", "</blockquote>")
subscription_link = get_display_subscription_link(subscription) subscription_link = get_display_subscription_link(subscription)
hide_subscription_link = settings.should_hide_subscription_link() hide_subscription_link = settings.should_hide_subscription_link()

View File

@@ -1276,8 +1276,8 @@
"SUBSCRIPTION_APPS_PROMPT": "Choose an app to connect:", "SUBSCRIPTION_APPS_PROMPT": "Choose an app to connect:",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Apps for {device_name}</b>", "SUBSCRIPTION_APPS_TITLE": "📱 <b>Apps for {device_name}</b>",
"SUBSCRIPTION_APP_NOT_FOUND": "❌ App not found", "SUBSCRIPTION_APP_NOT_FOUND": "❌ App not found",
"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "</blockquote>",
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 <b>Connected devices:</b>\n", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "<blockquote>📱 <b>Connected devices:</b>\n",
"SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Connect subscription</b>\n\n📱 Tap the button below to open the app:", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Connect subscription</b>\n\n📱 Tap the button below to open the app:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Connect subscription</b>\n\n🔗 <b>Subscription link:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Choose your device</b> to get detailed setup instructions:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Connect subscription</b>\n\n🔗 <b>Subscription link:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Choose your device</b> to get detailed setup instructions:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Connect subscription</b>\n\n The subscription link is available via the buttons below or in the “My subscription” section.\n\n💡 <b>Choose your device</b> to get detailed setup instructions:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Connect subscription</b>\n\n The subscription link is available via the buttons below or in the “My subscription” section.\n\n💡 <b>Choose your device</b> to get detailed setup instructions:",

View File

@@ -1288,8 +1288,8 @@
"SUBSCRIPTION_APPS_PROMPT": "Выберите приложение для подключения:", "SUBSCRIPTION_APPS_PROMPT": "Выберите приложение для подключения:",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Приложения для {device_name}</b>", "SUBSCRIPTION_APPS_TITLE": "📱 <b>Приложения для {device_name}</b>",
"SUBSCRIPTION_APP_NOT_FOUND": "❌ Приложение не найдено", "SUBSCRIPTION_APP_NOT_FOUND": "❌ Приложение не найдено",
"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "</blockquote>",
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 <b>Подключенные устройства:</b>\n", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "<blockquote>📱 <b>Подключенные устройства:</b>\n",
"SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Подключить подписку</b>\n\n📱 Нажмите кнопку ниже, чтобы открыть приложение:", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Подключить подписку</b>\n\n📱 Нажмите кнопку ниже, чтобы открыть приложение:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Подключить подписку</b>\n\n🔗 <b>Ссылка подписки:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Подключить подписку</b>\n\n🔗 <b>Ссылка подписки:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Подключить подписку</b>\n\n Ссылка подписки доступна по кнопкам ниже или в разделе «Моя подписка».\n\n💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Подключить подписку</b>\n\n Ссылка подписки доступна по кнопкам ниже или в разделе «Моя подписка».\n\n💡 <b>Выберите ваше устройство</b> для получения подробной инструкции по настройке:",

View File

@@ -1280,8 +1280,8 @@
"SUBSCRIPTION_APPS_PROMPT": "Оберіть додаток для підключення:", "SUBSCRIPTION_APPS_PROMPT": "Оберіть додаток для підключення:",
"SUBSCRIPTION_APPS_TITLE": "📱 <b>Додатки для {device_name}</b>", "SUBSCRIPTION_APPS_TITLE": "📱 <b>Додатки для {device_name}</b>",
"SUBSCRIPTION_APP_NOT_FOUND": "❌ Додаток не знайдено", "SUBSCRIPTION_APP_NOT_FOUND": "❌ Додаток не знайдено",
"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "", "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER": "</blockquote>",
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "📱 <b>Підключені пристрої:</b>\n", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE": "<blockquote>📱 <b>Підключені пристрої:</b>\n",
"SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Підключити підписку</b>\n\n📱 Натисніть кнопку нижче, щоб відкрити додаток:", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE": "🚀 <b>Підключити підписку</b>\n\n📱 Натисніть кнопку нижче, щоб відкрити додаток:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Підключити підписку</b>\n\n🔗 <b>Посилання підписки:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Оберіть ваш пристрій</b> для отримання детальної інструкції з налаштування:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE": "📱 <b>Підключити підписку</b>\n\n🔗 <b>Посилання підписки:</b>\n<code>{subscription_url}</code>\n\n💡 <b>Оберіть ваш пристрій</b> для отримання детальної інструкції з налаштування:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Підключити підписку</b>\n\n Посилання підписки доступне за кнопками нижче або в розділі «Моя підписка».\n\n💡 <b>Оберіть ваш пристрій</b> для отримання детальної інструкції з налаштування:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN": "📱 <b>Підключити підписку</b>\n\n Посилання підписки доступне за кнопками нижче або в розділі «Моя підписка».\n\n💡 <b>Оберіть ваш пристрій</b> для отримання детальної інструкції з налаштування:",

View File

@@ -1279,8 +1279,8 @@
"SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:", "SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:",
"SUBSCRIPTION_APPS_TITLE":"📱<b>适用于{device_name}的应用程序</b>", "SUBSCRIPTION_APPS_TITLE":"📱<b>适用于{device_name}的应用程序</b>",
"SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序", "SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序",
"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"", "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"</blockquote>",
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"📱<b>已连接设备:</b>\n", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"<blockquote>📱<b>已连接设备:</b>\n",
"SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀<b>连接订阅</b>\n\n📱点击下方按钮打开应用程序", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀<b>连接订阅</b>\n\n📱点击下方按钮打开应用程序",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱<b>连接订阅</b>\n\n🔗<b>订阅链接:</b>\n<code>{subscription_url}</code>\n\n💡<b>请选择您的设备</b>以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱<b>连接订阅</b>\n\n🔗<b>订阅链接:</b>\n<code>{subscription_url}</code>\n\n💡<b>请选择您的设备</b>以获取详细设置说明:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱<b>连接订阅</b>\n\n订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡<b>请选择您的设备</b>以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱<b>连接订阅</b>\n\n订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡<b>请选择您的设备</b>以获取详细设置说明:",
@@ -1606,8 +1606,8 @@
"SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:", "SUBSCRIPTION_APPS_PROMPT":"请选择要连接的应用程序:",
"SUBSCRIPTION_APPS_TITLE":"📱<b>适用于{device_name}的应用程序</b>", "SUBSCRIPTION_APPS_TITLE":"📱<b>适用于{device_name}的应用程序</b>",
"SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序", "SUBSCRIPTION_APP_NOT_FOUND":"❌未找到应用程序",
"SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"", "SUBSCRIPTION_CONNECTED_DEVICES_FOOTER":"</blockquote>",
"SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"📱<b>已连接设备:</b>\n", "SUBSCRIPTION_CONNECTED_DEVICES_TITLE":"<blockquote>📱<b>已连接设备:</b>\n",
"SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀<b>连接订阅</b>\n\n📱点击下方按钮打开应用程序", "SUBSCRIPTION_CONNECT_CUSTOM_MESSAGE":"🚀<b>连接订阅</b>\n\n📱点击下方按钮打开应用程序",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱<b>连接订阅</b>\n\n🔗<b>订阅链接:</b>\n<code>{subscription_url}</code>\n\n💡<b>请选择您的设备</b>以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE":"📱<b>连接订阅</b>\n\n🔗<b>订阅链接:</b>\n<code>{subscription_url}</code>\n\n💡<b>请选择您的设备</b>以获取详细设置说明:",
"SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱<b>连接订阅</b>\n\n订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡<b>请选择您的设备</b>以获取详细设置说明:", "SUBSCRIPTION_CONNECT_DEVICE_MESSAGE_HIDDEN":"📱<b>连接订阅</b>\n\n订阅链接在下方按钮中或“我的订阅”部分可用。\n\n💡<b>请选择您的设备</b>以获取详细设置说明:",

View File

@@ -1,5 +1,3 @@
import html
import re
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
@@ -65,14 +63,6 @@ def append_privacy_hint(text: str | None, language: str | None) -> str:
return hint 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]: def prepare_privacy_safe_kwargs(kwargs: Dict[str, Any] | None = None) -> Dict[str, Any]:
safe_kwargs: Dict[str, Any] = dict(kwargs or {}) safe_kwargs: Dict[str, Any] = dict(kwargs or {})
safe_kwargs.pop("reply_markup", None) 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) safe_kwargs = prepare_privacy_safe_kwargs(kwargs)
return await _original_answer(self, fallback_text, **safe_kwargs) return await _original_answer(self, fallback_text, **safe_kwargs)
# Фоллбек, если Telegram ругается на caption или другое ограничение: отправим как текст # Фоллбек, если Telegram ругается на caption или другое ограничение: отправим как текст
fallback_text = _strip_html(text) return await _original_answer(self, text, **kwargs)
safe_kwargs = dict(kwargs)
safe_kwargs.pop("parse_mode", None)
return await _original_answer(self, fallback_text, **safe_kwargs)
except Exception: except Exception:
return await _original_answer(self, text, **kwargs) return await _original_answer(self, text, **kwargs)
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() await self.delete()
except Exception: except Exception:
pass pass
fallback_text = _strip_html(text) return await _original_answer(self, text, **kwargs)
safe_kwargs = dict(kwargs)
safe_kwargs.pop("parse_mode", None)
return await _original_answer(self, fallback_text, **safe_kwargs)
return await _original_edit_text(self, text, **kwargs) return await _original_edit_text(self, text, **kwargs)
@@ -178,3 +162,4 @@ def patch_message_methods():
return return
Message.answer = _answer_with_photo Message.answer = _answer_with_photo
Message.edit_text = _edit_with_photo Message.edit_text = _edit_with_photo

View File

@@ -1,6 +1,3 @@
import html
import re
from aiogram import types from aiogram import types
from aiogram.exceptions import TelegramBadRequest from aiogram.exceptions import TelegramBadRequest
from aiogram.types import FSInputFile, InputMediaPhoto from aiogram.types import FSInputFile, InputMediaPhoto
@@ -36,14 +33,6 @@ def _get_language(callback: types.CallbackQuery) -> str | None:
return 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): def _build_base_kwargs(keyboard: types.InlineKeyboardMarkup | None, parse_mode: str | None):
kwargs: dict[str, object] = {} kwargs: dict[str, object] = {}
if parse_mode is not None: if parse_mode is not None:
@@ -68,19 +57,6 @@ async def _answer_text(
kwargs = prepare_privacy_safe_kwargs(kwargs) kwargs = prepare_privacy_safe_kwargs(kwargs)
kwargs.setdefault("parse_mode", parse_mode or "HTML") 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( await callback.message.answer(
caption, caption,

View File

@@ -4,13 +4,14 @@ from datetime import datetime
import html import html
ALLOWED_HTML_TAGS = { ALLOWED_HTML_TAGS = {
'b', 'strong', 'b', 'strong',
'i', 'em', 'i', 'em',
'u', 'ins', 'u', 'ins',
's', 'strike', 'del', 's', 'strike', 'del',
'code', 'code',
'pre', 'pre',
'a' 'a',
'blockquote'
} }
SELF_CLOSING_TAGS = { SELF_CLOSING_TAGS = {
@@ -145,24 +146,6 @@ def sanitize_html(text: str) -> str:
return text 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"&lt;/?blockquote[^&]*&gt;\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"<i>❝ {clean_text} ❞</i>"
def sanitize_telegram_name(name: Optional[str]) -> Optional[str]: def sanitize_telegram_name(name: Optional[str]) -> Optional[str]:
"""Санитизация Telegram-имени для безопасной вставки в HTML и хранения. """Санитизация Telegram-имени для безопасной вставки в HTML и хранения.
Заменяет угловые скобки и амперсанд на безопасные визуальные аналоги. Заменяет угловые скобки и амперсанд на безопасные визуальные аналоги.