Разбивка сообщений¶
Как одна HTML-страница превращается в несколько сообщений мессенджера, почему это происходит и что с этим можно сделать.
Документ описывает фактическое поведение кода, а не желаемое. Места, где поведение расходится со спецификацией или выглядит как дефект, помечены ⚠️ и собраны в бэклог.
Почему сообщение делится¶
Пять причин, из которых растут все остальные:
- Подпись к медиа ограничена 1024 символами, а текст сообщения — 4096. Если текст не влезает в подпись, фото и текст приходится разносить.
- Лимит длины текста. Telegram — 4096 символов, MAX — 4000.
- У сообщения ровно один
reply_markup. Нельзя одновременно убрать reply-клавиатуру и показать inline-кнопки. - Один тип контента на сообщение. Текст, фото, документ, геолокация и запрос контакта — это разные API-методы.
- Внутри сообщения нет блочной вёрстки. Заголовки, списки и таблицы в обычном
parse_mode=HTMLне поддерживаются, поэтому «страницу» верстают несколькими.bf-message.
Матрица разбивки¶
| # | Триггер | Что получает пользователь | Где в коде |
|---|---|---|---|
| 1 | Текст длиннее 4096 символов | N сообщений, кнопки под последним | telegram_api.py, html_chunks.py |
| 2 | Одно фото и подпись длиннее 1024 символов | Фото без подписи, следом текст с кнопками | core/bot_impl.py:230-277, botfarm/celery.py:670-778 |
| 3 | Два и более фото | Альбом без подписей, следом текст с кнопками | core/bot_impl.py:279-312 |
| 4 | Есть inline-кнопки И надо убрать reply-клавиатуру | «⏳» → удаление → настоящее сообщение (3 вызова API) | core/bot_impl.py:560-582, botfarm/celery.py:1174-1192 |
| 5 | Несколько .bf-message на странице |
По сообщению на блок | core/views/page_sender.py:975 |
| 6 | .bf-location, .bf-request-contact |
Всегда отдельное сообщение | page_sender.py:1149-1230, 1232-1248 |
| 7 | MAX: подпись длиннее 4000 символов | Фото, следом текст | core/bot_impl.py:1037-1119 |
| 8 | Instagram: любое количество фото | N фото + текст = N+1 сообщений | core/bot_impl.py |
| 9 | Instagram: вложение и текст | Всегда раздельно | core/bot_impl.py:1358-1396 |
| 10 | Ошибка 400 при редактировании | Удаление + отправка заново | botfarm/celery.py:372-452 |
Случай №5 — единственный спроектированный. Остальные девять — обход технических ограничений.
Как это выглядит для пользователя¶
Фото теряет подпись (случай №2). Пришло фото, под ним пусто, следующим сообщением — текст.
Порог — 1024 символа, как и у самой платформы (bot_impl.py:238, page_sender.py:559).
Раньше проверка считала UTF-8 байты, и кириллическая подпись разносилась на два сообщения уже примерно на 512 символах — вдвое раньше, чем требует Telegram. Исправлено.
Альбом всегда без подписей (случай №3). В sendMediaGroup подпись каждого элемента жёстко
выставлена в пустую строку (bot_impl.py:292), текст уезжает отдельным сообщением с задержкой в
секунду, чтобы не обогнать альбом.
Раньше второе сообщение отправлялось только при непустом reply_markup, поэтому страница
с картинками и без единой кнопки теряла текст бесследно. Теперь сообщение уходит всегда, когда
есть текст (bot_impl.py:302-312); если текста нет, но есть кнопки, подставляется невидимый
символ — Telegram отклоняет sendMessage с пустым text.
Мигание «⏳» (случай №4). Возникает при переходе со страницы с .bf-request-contact на
страницу с inline-кнопками. Telegram не даёт совместить ReplyKeyboardRemove и
InlineKeyboardMarkup в одном reply_markup, поэтому код шлёт заглушку, удаляет её и отправляет
настоящее сообщение.
Нарезка длинного текста (случай №1). Клавиатура оказывается под последним сообщением,
id остальных сохраняются в ChatPage.extra_message_ext_id и удаляются при следующей навигации.
Поле — CharField(64), туда помещается около шести id; если чанков окажется больше, лишние
останутся в чате, о чём пишется предупреждение в лог.
Разбивка или обрезка¶
Это разные вещи, и на страничном пути сейчас работает вторая.
Нарезка реализована и корректна — html_chunks.split_html_text. Она режет текст только по
границам «атомов» (тег, HTML-сущность или один символ), поэтому никогда не рвёт разметку,
а теги, оставшиеся открытыми на границе, закрывает в конце чанка и переоткрывает в начале
следующего вместе с атрибутами:
invoke_telegram (telegram_api.py:87) вешает клавиатуру только на последнее сообщение и
возвращает его ответ — именно оно становится message_ext_id страницы. Id остальных сообщений
складываются в список extra_message_ids, попадают в ChatPage.extra_message_ext_id через
запятую и удаляются при следующей навигации. Если какой-то чанк не отправился, отправка
прерывается и наверх возвращается ответ упавшего чанка, а не мнимый успех последнего.
Нарезка включена: TGImpl.send_message больше не обрезает текст. Предохранитель
MAX_MESSAGE_CHUNKS = 6 не даёт странице-аномалии превратиться в десятки сообщений — всё,
что не влезло в шесть чанков, отбрасывается с предупреждением в лог.
⚠️ editMessageText резать не умеет. Одно сообщение нельзя отредактировать в три, поэтому
страницу длиннее лимита _send_page_message отправляет новым сообщением и удаляет прежнее
(page_sender.py:556-566). Для страниц короче 4096 ничего не меняется — они по-прежнему
редактируются на месте, и переход по кнопке выглядит как раньше.
Сегодня нарезка реально срабатывает только у вызывающих в обход TGImpl
(core/tg_update_processor.py:37,97,108,175,230,258, core/views/hook_tg.py:64,414,
botfarm/celery.py:393,584,712,1186), а они шлют короткие константы.
Как считается длина¶
trim_text меряет длину в кодовых единицах UTF-16 (util.text_length) — ровно так же, как
это делает Telegram. Для латиницы и кириллицы это совпадает с числом символов, эмодзи вне BMP
занимают две единицы. Та же мера применяется к MAX, у которого лимит 4000 символов
(POST /messages).
Раньше trim_text считал UTF-8 байты, из-за чего русский текст обрезался примерно вдвое раньше:
на ~2045 символах в Telegram и ~1998 в MAX. Исправлено, лимиты вынесены в константы
TG_TEXT_LIMIT и TG_CAPTION_LIMIT (bot_impl.py:30-31).
Расхождение платформ¶
| Telegram | MAX | VK | ||
|---|---|---|---|---|
| Лимит текста | 4096 символов | 4000 символов | не ограничивается кодом | не ограничивается кодом |
| Фактическая обрезка | 4096 символов | 4000 символов | нет | нет |
| Подпись к медиа | 1024 символа | общий лимит текста | — | не поддерживается |
| Альбом | sendMediaGroup, до 10 |
одно сообщение, до 12 вложений | одно сообщение, до 10 вложений | не реализован |
| Нарезка длинного текста | есть | нет, обрезка | нет | нет |
VK: альбом одним сообщением. VKImpl.send_photos (bot_impl.py:750-779) загружает все
картинки и отправляет их одним messages.send — до 10 вложений, вместе с текстом и клавиатурой.
Разбивки у VK нет вообще. Лишние картинки сверх десяти отбрасываются с предупреждением в лог.
Раньше метод не был переопределён. Ошибки при этом не возникало: BaseImpl объявлен обычным
классом без ABCMeta (bot_impl.py:94), поэтому @abc.abstractmethod (bot_impl.py:126-128)
ничего не проверяет и вызов молча возвращал None — страница с двумя и более картинками у
VK-бота не отправлялась совсем. Исправлено.
MAX: альбом одним сообщением. До 12 вложений вместе с клавиатурой и текстом; при наличии кнопок под картинки остаётся на одно место меньше. Лишние отбрасываются с предупреждением.
MAX: клавиатура переносится по лимитам платформы. До 7 кнопок в ряду и до 3, если в ряду
есть link, open_app, request_geo_location или request_contact. Ряд из четырёх .bf-url
кнопок раньше отклонялся платформой, теперь переносится на следующую строку.
Как уменьшить число сообщений уже сегодня¶
Рекомендации авторам страниц, без изменений в коде:
- Держите подпись к фото в пределах ~500 символов кириллицей. Порог считается в байтах: 1024 байта это 1024 латинских или ~512 кириллических символа.
- Не смешивайте
.bf-request-contactи inline-кнопки на соседних экранах, если важна скорость отклика — переход между ними стоит трёх вызовов API вместо одного. - Одна картинка вместо альбома сохраняет подпись в том же сообщении (пока текст влезает в 1024 байта). Альбом подпись теряет всегда.
<blockquote>работает и в Telegram, и в MAX — им можно визуально отделить блок текста, не заводя второй.bf-message. Вариант<blockquote expandable>понимает только Telegram, MAX покажет обычную цитату.
См. также¶
- Форматирование Telegram — какие форматы поддерживаются сейчас, что можно добавить и как это уменьшит разбивку
- HTML спецификация — разметка
bf-*