Форматирование 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-сущностей поддерживается больше, чем в обычном режиме:
… — – ‘ ’ “ ” '.
Что это снимает¶
| Случай разбивки | После 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>на странице по-прежнему игнорируются.
См. также¶
- Разбивка сообщений — где и почему сообщение делится на несколько
- HTML спецификация — разметка
bf-*