Перейти к содержанию

Форматирование Telegram

Что из возможностей форматирования мы используем сегодня, что можно добавить и как каждая добавка влияет на разбивку сообщения на несколько.


Что работает сейчас

Санитизации исходящего HTML в проекте нет: что бэкенд положил в .bf-text, то и уходит в мессенджер (page_sender.py:1254 только схлопывает пробелы и меняет <br> на перевод строки). Фактический набор тегов задаёт сама платформа, а ошибка ловится постфактум — botfarm/celery.py:459-538 разбирает ответ 400 can't parse entities и шлёт фрагмент вокруг ошибки в сервисный чат.

Поэтому «поддерживается» ниже означает «платформа примет и отрисует».

Тег Telegram MAX
<b>, <strong> да да
<i>, <em> да да
<u>, <ins> да да
<s>, <del> да да
<strike> да не заявлен
<code> да да
<pre> да да
<pre><code class="language-…"> да, с подсветкой тег пройдёт, подсветки нет
<a href="…"> да да
<blockquote> да да
<blockquote expandable> да атрибут игнорируется, будет обычная цитата
<tg-spoiler>, <span class="tg-spoiler"> да нет
<tg-emoji emoji-id="…"> да, с ограничениями нет
<tg-time unix="…" format="…"> да нет
<mark> нет да
<h1>…<h6> нет да, все уровни одинаково
<p>, <ul>, <ol>, <li>, <table>, <div> нет нет

Источники: Telegram HTML style, MAX — форматирование текста.

В html-spec.md описана только часть

Документация разметки перечисляет b, strong, i, em, u, s, strike, del, code, pre, a, br. blockquote, blockquote expandable, tg-spoiler, tg-emoji и ins работают уже сейчас, но там не упомянуты.

Упоминания

<a href="bf://user?id=N">Имя</a> разворачивается в платформенную ссылку (core/api_views.py:131-165): tg://user?id=… для Telegram, max://user/… для MAX, обычная ссылка на профиль для кросс-платформенных случаев. Работает только при parse_mode (.bf-html или .bf-markdown2), для .bf-plain — нет.


Что можно добавить без новых методов API

Всё перечисленное укладывается в текущий parse_mode=HTML и текущие вызовы.

Возможность Что даёт Влияние на разбивку MAX
link_preview_options с url, prefer_large_media, show_above_text Картинка над текстом без sendPhoto Снимает случай №2: изображение + до 4096 символов + кнопки одним сообщением Есть только выключатель disable_link_preview, трюк не воспроизводится
caption у первого элемента sendMediaGroup До 1024 символов прямо в альбоме Частично снимает №3 и чинит потерю текста при отсутствии кнопок Своя схема вложений, см. ниже
Одно сообщение MAX со всеми вложениями До 12 изображений/видео + клавиатура + текст Снимает случай №8 для MAX (для VK уже сделано) Прямо предусмотрено API
<blockquote expandable> Длинный блок сворачивается в клиенте Меньше поводов делить текст «по смыслу» Покажет обычную цитату
show_caption_above_media Подпись над медиа Косметика нет
has_spoiler у фото Картинка под спойлером Новая возможность разметки нет
<tg-time> Дата/время в часовом поясе пользователя — нет

Сегодня из link_preview_options используется единственное поле is_disabled (bot_impl.py), по умолчанию True. Переопределить его можно только через API /sendMessage (core/serializers.py:46, core/api_views.py:242-244) — на страничном пути page_sender.py параметр не передаёт, поэтому для страниц превью выключено всегда.


Кросс-платформенные последствия

Расширение набора тегов — не нейтральная операция

Один и тот же .bf-text уходит и в Telegram, и в MAX. MaxImpl.send_message (bot_impl.py) выставляет format="html" для всего, что не MarkdownV2 — включая .bf-plain. Развести текст по платформам нечем: bf-only-tg / bf-only-max и т.п. (core/views/buttons.py:90-101) работают только для кнопок, у .bf-message такого механизма нет.

Наборы тегов пересекаются не полностью и расходятся в обе стороны:

  • <tg-spoiler>, <tg-emoji>, <tg-time>, blockquote expandable — Telegram понимает, MAX нет;
  • <mark>, <h1>…<h6> — MAX понимает, Telegram отклонит сообщение целиком с 400 can't parse entities.

Два способа это развязать:

Вариант А — фильтр на стороне платформы. Каждый Impl вычищает из HTML теги, которых его платформа не знает (<tg-spoiler> → текст без разметки, <h2> → жирная строка + перевод строки). Плюс: страницы остаются платформенно-нейтральными, авторам ничего не надо знать. Минус: нужен разбор HTML на отправке, которого сейчас нет вовсе.

Вариант Б — платформенные классы на .bf-message. По образцу PLATFORM_CLASSES (core/views/buttons.py:67) разрешить bf-only-tg / bf-only-max на самом сообщении, чтобы автор мог дать два варианта блока. Плюс: механизм уже есть, расширение маленькое. Минус: дублирование контента на стороне автора страницы и рост числа .bf-message, то есть больше сообщений, а не меньше.

Рекомендация — вариант А. Он решает задачу в одном месте, не требует правок на страницах и не увеличивает разбивку. Вариант Б стоит добавить позже как точечное дополнение, когда нужен принципиально разный контент, а не разная разметка одного и того же.


Что требует новых методов API

Не реализовано в коде вообще:

  • sendVideo, sendAudio, sendAnimation, sendVoice. Теги <video> и <audio> на странице сейчас просто игнорируются — page_sender.py:1240 собирает только <img>. MAX умеет video, audio и file как типы вложений.
  • entities вместо parse_mode. Позиционная разметка снимает весь класс ошибок «разорванный тег» и делает нарезку длинного текста безопасной. Ценой того, что разметку придётся вычислять самим.
  • message_effect_id — визуальные эффекты, только для приватных чатов.

Rich Messages

Bot API 10.1 (11 июня 2026) и 10.2 (14 июля 2026) добавили принципиально другой способ отправки: sendRichMessage.

Пока только исследование

В код не заводим до проверки реальной поддержки клиентами. Раздел нужен, чтобы понимать, какие из наших ограничений скоро перестанут быть ограничениями.

Как устроено

sendRichMessage принимает InputRichMessage, где заполнено ровно одно из полей html, markdown или blocks, плюс массив media для файлов, на которые ссылаются tg://photo?id=, tg://video?id=, tg://audio?id=. Поддерживает reply_markup (включая обычную inline-клавиатуру), reply_parameters, message_effect_id.

Отдельно важно: editMessageText получил параметр rich_message. Наш основной путь навигации — редактирование сообщения на месте при нажатии кнопки — с Rich Messages работает.

Есть sendRichMessageDraft для стриминга генерации: черновик живёт 30 секунд, поддерживает тег <tg-thinking>, финальный результат всё равно нужно закрепить через sendRichMessage.

Лимиты

Параметр Значение
Длина текста 32768 символов
Блоков, включая вложенные 500
Уровней вложенности 16
Медиавложений 50
Колонок в таблице 20

Что появляется в разметке

Блочные элементы: <h1>…<h6>, <p>, <hr>, <footer>, <ul> / <ol> (с start, type, reversed, value и чекбоксами через <input type="checkbox">), <blockquote> с <cite>, <aside> (выносная цитата), <details> / <summary>, <table> с bordered, striped, colspan, rowspan, align, valign и <caption>, <pre><code class="language-…">.

Медиа как отдельные блоки: <img>, <video>, <audio>, <figure> / <figcaption>, <tg-collage>, <tg-slideshow>, <tg-map lat lon zoom>.

Строчные: <mark>, <sub>, <sup>, <tg-spoiler>, <tg-emoji>, <tg-time>, <tg-math> / <tg-math-block> (LaTeX), сноски через <tg-reference name> и <a href="#…">, ссылки mailto:, tel:, tg://user.

Именованных HTML-сущностей поддерживается больше, чем в обычном режиме: &nbsp; &hellip; &mdash; &ndash; &lsquo; &rsquo; &ldquo; &rdquo; &apos;.

Что это снимает

Случай разбивки После Rich Messages
№1 текст длиннее 4096 Уходит — лимит 32768
№2 фото и длинная подпись Уходит — медиа это блок внутри сообщения, отдельной подписи нет
№3 альбом и кнопки Уходит — <tg-collage> / <tg-slideshow> + текст + reply_markup одним вызовом
№5 несколько .bf-message ради вёрстки Уходит — заголовки, списки, таблицы и <details> внутри одного сообщения
№4 remove_keyboard и inline-кнопки Остаётся — это ограничение reply_markup, а не контента
№6 геолокация и запрос контакта Остаётся — <tg-map> это картинка карты, а не sendLocation

Риски

  • API вышло за месяц-два до написания документа, реальную поддержку клиентами мы не проверяли.
  • Поведение на старых клиентах в документации не описано.
  • У MAX, VK и Instagram аналога нет — расхождение между платформами вырастет, и любой .bf-rich придётся деградировать до текущего плоского HTML.
  • Медиа в режимах html и markdown — только HTTP(S)-ссылки либо предзагруженные через media.

Возможная точка входа

Новый класс .bf-rich на .bf-message рядом с bf-html / bf-plain / bf-markdown2 — развилка режимов уже существует в page_sender.py:1323, добавление ветки туда не ломает существующие страницы.


Бэклог фиксов

Приоритет по принципу «сначала то, что молча теряет контент пользователя».

P0 — потеря данных (исправлено)

# Проблема Платформы Исправление
B1 trim_text считал байты, лимиты платформ — символы. Кириллица резалась вдвое Telegram, MAX Длина считается в кодовых единицах UTF-16 через util.text_length; лимиты вынесены в TG_TEXT_LIMIT / TG_CAPTION_LIMIT (bot_impl.py:30-31)
B1a Проверка «влезает ли в подпись» тоже в байтах — фото и текст разносились вдвое раньше срока Telegram bot_impl.py:238, page_sender.py:559
B2 Альбом без кнопок терял текст страницы целиком Telegram Сообщение уходит всегда, когда есть текст; при пустом тексте с кнопками подставляется EMPTY_MESSAGE_TEXT (bot_impl.py:302-312)
B3 VKImpl.send_photos не был реализован и молча возвращал None VK Реализован: все вложения одним messages.send, до 10 (bot_impl.py:750-779)
B4 send_photos слал кнопки только при непустом тексте MAX bot_impl.py:1121-1167
B5 caption_fits мерял символы, обрезка рядом — байты MAX Обе проверки используют text_length (bot_impl.py:1084)

Тесты: core/tests/test_trim_text.py, core/tests/test_caption_fits.py, core/tests/test_send_photos.py.

Корректность нарезки (исправлено)

# Проблема Исправление
B6 Точка разреза искалась только по </pre>, </code>, </a> — <b>, <i>, <u>, <s>, <blockquote>, <tg-spoiler> рвались, HTML-сущности тоже Текст режется только по границам атомов — тег, сущность или символ (html_chunks.py)
B7 Незакрытые pre / code дозакрывались, но не переоткрывались в следующем чанке Любой открытый тег закрывается в конце чанка и переоткрывается в начале следующего вместе с атрибутами
B8 Возвращался ответ только последнего чанка — остальные сообщения не удалялись при навигации invoke_telegram складывает их id в extra_message_ids, они попадают в ChatPage.extra_message_ext_id через запятую и удаляются в sent_message_callback. При ошибке чанка отправка прерывается и возвращается ответ упавшего

Тесты: core/tests/test_html_chunks.py, core/tests/test_send_message_chunks.py, core/tests/test_extra_messages_removal.py.

Разбивка включена

TGImpl.send_message больше не обрезает текст — нарезкой занимается invoke_telegram. editMessageText резать не умеет, поэтому страницу длиннее лимита _send_page_message отправляет новым сообщением и удаляет прежнее. Предохранитель MAX_MESSAGE_CHUNKS = 6 не даёт странице-аномалии превратиться в десятки сообщений.

P1 — корректность (исправлено)

# Проблема Платформы Исправление
B9 Нет ограничения на 10 элементов в sendMediaGroup Telegram TGImpl.MEDIA_GROUP_LIMIT, лишние отбрасываются с предупреждением
B10 Фото отправлялись по одному, хотя API принимает до 12 вложений с клавиатурой в одном сообщении MAX MaxImpl.send_photos шлёт одно сообщение; при наличии кнопок под картинки остаётся на место меньше
B11 Ряд из 4+ «широких» кнопок превышал лимит MAX MAX _build_inline_keyboard переносит лишние кнопки на следующий ряд: 7 в ряду, 3 если есть link / request_contact; 30 рядов, 210 кнопок

P2 — гигиена (исправлено)

# Проблема Исправление
B12 parse_mode, numeric_payload и прочие служебные ключи улетали в VK API VKImpl._clean_kwargs отсеивает их; попутно error_callback наконец доходит до invoke_vk_task — до этого ошибки VK не репортились вообще
B13 format="html" выставлялся даже для .bf-plain MaxImpl._format_text: без parse_mode поле format не отправляется
B14 Значение parse_mode в API /sendMessage не валидировалось ChoiceField с HTML / MarkdownV2 / Markdown
B15 \s+ схлопывался до замены <br>, <br class="…"> не распознавался extract_message_text: <br> в любом написании подменяется плейсхолдером до схлопывания
B16 Устаревший disable_web_page_preview link_preview_options
B17 Выбор parse_mode не был покрыт тестами Вынесен в parse_mode_for_message, покрыт core/tests/test_parse_mode_selection.py

Тесты: test_media_limits.py, test_max_keyboard_limits.py, test_max_format.py, test_vk_kwargs.py, test_bf_text_extraction.py, test_parse_mode_selection.py, test_long_message_delivery.py.

Открытые вопросы

Бэклог закрыт. Осталось то, что требует решения, а не починки:

  • .bf-rich поверх sendRichMessage — см. раздел выше. Ждём проверки поддержки клиентами.
  • Расширение набора тегов в .bf-html — упирается в общий .bf-text для Telegram и MAX, см. «Кросс-платформенные последствия».
  • Видео и аудио — <video> и <audio> на странице по-прежнему игнорируются.

См. также