25 августа 2026 года Telegram представил кнопки внутри Rich Messages на примере шахмат. В демонстрационном боте доска, фигуры, подсказка и элементы управления находились прямо внутри одного сообщения. Пользователь выбирал фигуру, затем клетку, бот получал callback и перерисовывал ту же доску. Никакого отдельного сайта или Mini App для этого не требовалось.

Официальная демонстрация Telegram: шахматная доска и элементы управления внутри Rich Message.

Для разработки таких сообщений нужен как минимум Bot API 10.3. На стороне пользователя требуется клиент Telegram с поддержкой встроенных кнопок. Единого номера версии для всех платформ Telegram в анонсе не указал и рекомендует обновиться до последней версии. Для Telegram Desktop точной отправной точкой является версия 7.1.0.

Больше всего меня зацепило, что весь интерфейс фактически описывается текстовой разметкой, а Telegram сам превращает ее в нативные таблицы и кнопки. Как только я увидел эту демонстрацию, сразу захотелось проверить технологию на сценарии с более насыщенным состоянием.

Для эксперимента я взял Texas Hold’em. Покер оказался удобным стендом: несколько участников, общая и скрытая информация, меняющийся набор действий, банк и последовательность серверных переходов. Если такая модель справится с карточным столом, ей точно найдется применение в опросах, конфигураторах, пошаговых формах и небольших играх.

Эксперимент с Texas Hold'em: стол, карты, банк и действия работают внутри одного Rich Message.
Эксперимент с Texas Hold'em: стол, карты, банк и действия работают внутри одного Rich Message.

Живой результат эксперимента можно открыть прямо в Telegram: @CorvenhallPokerBot. Это партия на виртуальных фишках, без депозитов, вывода средств и перехода в Mini App. Один человек играет против четырех серверных ботов, а стол и все действия остаются внутри сообщения.

В результате главным открытием для меня стали даже не таблицы и кнопки. Интереснее оказалась сама модель: Rich Message можно воспринимать как небольшой server‑driven интерфейс. Telegram‑клиент рисует нативные элементы, сервер хранит состояние, а каждое нажатие превращается в новый снимок того же сообщения.

Общий цикл Rich Message
Общий цикл Rich Message

Что изменилось в Bot API

Обычный Telegram‑бот исторически строил интерфейс из текста, вложения и клавиатуры под сообщением. Этого достаточно для меню или простого диалога, но сложное состояние быстро расползается по цепочке сообщений. Пользователь видит старые кнопки, бот публикует новые экраны ниже, а чат постепенно превращается в журнал работы приложения.

В Bot API 10.1 появились Rich Messages. Это отдельный тип содержимого с заголовками, таблицами, списками, цитатами, медиа, формулами и другими структурными блоками. Сообщение можно описать одним из трех способов:

  1. Rich HTML.

  2. Rich Markdown.

  3. Явным массивом типизированных блоков.

Одновременно использовать несколько представлений нельзя. На практике это хорошее ограничение: у приложения должен быть один понятный источник разметки.

В Bot API 10.3 Telegram добавил кнопки непосредственно внутрь Rich Message. В официальном анонсе в качестве примера как раз использовались шахматы: одно сообщение можно разместить в личном чате, группе или канале, а нажатия пользователей обрабатывать отдельно.

Важно не перепутать Rich HTML с браузером. Здесь нет DOM, JavaScript и CSS. HTML выступает компактным языком описания нативных блоков Telegram. Как именно будет выглядеть таблица, отступы и фон кнопки, решает клиентское приложение.

В этом и состоит необычность подхода: весь интерфейс можно передать как текстовую разметку в поле html. Альтернативный вариант — JSON‑массив блоков. Бот не загружает frontend‑бандл, не открывает Mini App и не отправляет пользователя на отдельный сайт. Сервер передает структуру, а Telegram превращает ее в нативный интерфейс.

Для первого знакомства достаточно примерно такой структуры:

<h3>Состояние партии</h3>

<table bordered striped compact>
  <tr>
    <td align="left">Игрок A<br><b>скрыто</b></td>
    <td align="left">Игрок B<br><b>скрыто</b></td>
  </tr>
  <tr>
    <td colspan="2" align="center">
      <b>Общее состояние</b><br>
      данные, которые видят все участники
    </td>
  </tr>
</table>

<tg-button-row align="center">
  <tg-button type="callback_data" data="v=17;a=pass">Пропустить</tg-button>
  <tg-button type="callback_data" style="primary" data="v=17;a=act">Действовать</tg-button>
</tg-button-row>

Это намеренно абстрактный пример. Значения callback здесь вымышлены, а структура не связана с конкретным приложением. Но принцип тот же: таблица описывает текущий снимок, кнопки содержат короткие намерения пользователя.

Сообщение как проекция состояния

Первый очевидный подход — относиться к Rich Message как к новой системе верстки: сделать таблицу, подобрать подписи, добавить кнопки и дальше обновлять отдельные кусочки. Он быстро приводит к тому же классу проблем, который знаком по сложному фронтенду: визуальное состояние начинает жить отдельно от предметного.

Гораздо надежнее считать сообщение одноразовой проекцией. В приложении есть четыре независимых слоя:

Domain state содержит настоящее состояние сценария. Для пошаговой игры это участники, этап, доступные действия и результат предыдущего перехода.

Projection выбирает только то, что разрешено показать конкретному пользователю. Например, общие данные видят все, а персональные остаются персональными.

Renderer превращает проекцию в Rich HTML, Markdown или массив блоков. Он ничего не решает и не меняет состояние.

Transport отправляет новое сообщение или редактирует уже существующее через Bot API.

Из этого следует полезное правило: один и тот же state должен всегда давать один и тот же Rich Message. Если renderer зависит от случайности, времени выполнения или остатков предыдущей разметки, разбираться с ошибками будет неприятно.

В упрощенном виде код выглядит так:

state = repository.load(sessionRef)
view = makeProjection(state, viewer)
richMessage = render(view)

telegram.editMessage(
    chatId = viewer.chatId,
    messageId = state.activeMessageId,
    richMessage = richMessage
)

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

Что происходит после нажатия кнопки

Кнопка с callback_data не должна содержать команду в духе «измени поле X в базе». Это только намерение: выбрать вариант, подтвердить действие, открыть следующий уровень управления.

После нажатия Telegram присылает callback_query. Обработчик извлекает короткий непрозрачный payload, определяет сессию и проверяет, можно ли применить действие к текущему состоянию. Только после этого вызывается предметная логика.

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

Базовый конвейер можно записать так:

intent = callbackCodec.decode(query.data)
state = repository.load(intent.sessionRef)

if intent.expectedRevision != state.revision:
    answerCallback("Интерфейс уже обновился")
    return

commandId = stableCommandId(update.updateId)
nextState = domain.apply(state, intent.action, commandId)
repository.save(nextState)

projection = makeProjection(nextState, query.user)
editSameMessage(render(projection))
answerCallback()

Наивная реализация часто пропускает две проверки.

Первая — принадлежность события. Нельзя принимать номер сообщения и идентификатор сессии только потому, что они пришли от Telegram. Callback нужно связать с ожидаемым пользователем, чатом и активным сообщением.

Вторая — версия состояния. Между отрисовкой кнопки и нажатием мог произойти другой переход. Пользователь мог дважды нажать кнопку, открыть старое сообщение на другом устройстве или отправить два действия почти одновременно. Если payload был создан для ревизии 17, а сервер уже хранит ревизию 18, действие устарело.

Ревизия важнее красивой кнопки

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

Поэтому каждая изменяющая состояние кнопка должна быть привязана к версии, для которой она была отрисована. Версию необязательно передавать открытым числом. Можно использовать компактную ссылку на серверную запись, подписанный токен или иной непрозрачный формат. Важно, чтобы сервер мог определить: это действие относится к текущему снимку или к прошлому.

Проверка ревизии состояния
Проверка ревизии состояния

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

Это особенно заметно в игре. Кнопка «Пропустить» может быть допустима сейчас и бессмысленна через один переход. Но та же проблема встречается в обычных продуктах: старая цена, уже закрытая заявка, изменившийся статус заказа, использованный промокод или подтверждение операции из прошлого экрана.

Отдельно нужна идемпотентность. Получение Update, сохранение состояния и ответ Telegram не образуют одну общую транзакцию. Процесс может завершиться после записи в базу, но до подтверждения обработки. После перезапуска тот же Update нельзя превращать во второе действие. Стабильный идентификатор команды и уникальное ограничение на уровне хранилища решают проблему надежнее, чем проверка в памяти процесса.

Почему покер оказался полезным стендом

Шахматы хорошо показывают доску и выбор хода. Покер добавляет несколько неудобных для интерфейса свойств сразу.

Во‑первых, данные имеют разную видимость. Есть общая часть стола, персональные карты и скрытая информация других участников. Renderer не должен получать полный объект игры и самостоятельно решать, что спрятать. Безопасная проекция должна быть создана раньше.

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

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

Как состояние превращается в игровую таблицу
Как состояние превращается в игровую таблицу

Я остановился на стабильной таблице: участники занимают постоянные ячейки, общая информация находится в центре, персональная часть отделена, а набор кнопок строится из разрешенных действий. При каждом переходе меняются значения, но геометрия сообщения остается знакомой.

Здесь важна умеренность. Rich Message позволяет сделать сложную таблицу, но не дает полного контроля над пикселями. Если интерфейс держится только на точной ширине символа или одинаковом размере emoji на всех устройствах, он уже сломан. Лучше строить семантические блоки, которые сохраняют смысл при другом шрифте, ширине экрана и теме оформления.

Границы Rich Messages

У формата достаточно большие технические лимиты: до 32 768 UTF-8 символов, до 500 блоков, до 16 уровней вложенности, до 50 медиа и до 20 колонок таблицы. В одну строку блока кнопок можно поместить от одной до восьми кнопок. Для callback_data остается компактный диапазон от 1 до 64 байт.

Но максимальные значения не стоит воспринимать как цель. Таблица на 20 колонок формально пройдет API и почти наверняка окажется бесполезной на телефоне. Восемь текстовых кнопок в одну строку тоже редко будут хорошим решением.

Есть и более важные ограничения:

  • нет произвольного CSS и JavaScript;

  • нельзя управлять точной геометрией нативных компонентов;

  • высокочастотная анимация и непрерывный ввод не подходят этой модели;

  • каждое значимое действие проходит через сервер;

  • старому клиенту может потребоваться обновление для отображения нового формата;

  • ошибки сети и ограничения частоты запросов остаются частью архитектуры.

Последний пункт легко недооценить. Если один пользовательский жест запускает серию редактирований, промежуточные состояния лучше объединять. Пользователю важен подтвержденный результат, а не каждый внутренний шаг renderer.

Еще одна практическая мелочь: если в игровом или техническом тексте много @, #, похожих на команды строк и других автоматически распознаваемых сущностей, полезно осознанно настроить entity detection. Иначе Telegram может превратить часть состояния в ссылки и команды.

Когда Rich Message лучше Mini App

Rich Message не заменяет Mini App. Это другой компромисс.

Критерий

Rich Message

Mini App

Отображение

Нативные компоненты Telegram

Web‑интерфейс внутри Telegram

Развертывание UI

Разметка приходит через Bot API

Нужен отдельный frontend и hosting

Свобода дизайна

Ограниченная, клиент управляет видом

Почти полный контроль HTML, CSS и JavaScript

Взаимодействие

Дискретные нажатия и серверные переходы

Жесты, сложные формы, анимации, локальное состояние

Обновление

Обычно полная перерисовка сообщения

Обычная модель frontend‑приложения

Лучшие сценарии

Пошаговые процессы, статусы, опросы, игры, панели действий

Каталоги, редакторы, карты, графика, сложная навигация

Я бы выбирал Rich Message, если сценарий можно представить конечным автоматом с ограниченным набором действий. Пользователь смотрит на текущий снимок, выбирает одно действие и получает следующий снимок.

Mini App лучше, если пользователю нужно свободно перемещаться по экрану, вводить много данных, работать с графикой, перетаскивать объекты или получать быстрый локальный отклик без сетевого перехода после каждого жеста.

Выбор между Rich Message и Mini App
Выбор между Rich Message и Mini App

Что получилось в итоге

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

Оказалось, что технически сложная часть находится не в Rich HTML. Таблицу и кнопки собрать сравнительно легко. Основная работа начинается вокруг них: определить безопасную проекцию, сделать renderer детерминированным, привязать кнопки к ревизии, пережить повторную обработку событий и не дать старому интерфейсу изменить новое состояние.

В этом смысле Rich Message похож не на маленькую веб‑страницу, а на нативный документ с замкнутым серверным циклом событий. Он хорошо подходит для интерфейсов, где важны предсказуемость, небольшое количество действий и отсутствие перехода в отдельное приложение.

Покер здесь был только проверкой идеи. Тот же подход можно использовать для пошагового согласования, конфигуратора, панели дежурного, интерактивного отчета, учебного задания или небольшого turn‑based сценария в группе.

В следующей версии я хочу попробовать добавить режим с живыми игроками. Технически Telegram это позволяет: общий стол можно оставить в одном Rich Message, действия принимать через callback, а приватные карты показывать каждому участнику в отдельном эфемерном сообщении, которое видит только он, или в личном чате с ботом. Главная сложность здесь уже не в разметке, а в матчмейкинге, одновременных действиях, переподключении и восстановлении актуального состояния партии.

Если хочется посмотреть на технологию не по схеме, а в действии, рабочий пример находится здесь: @CorvenhallPokerBot.

И да, иногда полный интерфейс действительно может жить в одном сообщении. Главное — не пытаться превратить это сообщение в браузер.

Официальные материалы

Комментарии (3)


  1. AnatolyEmelin
    29.08.2026 09:14

    Спасибо. Полезно.


  1. achekalin
    29.08.2026 09:14

    Снова Макс будет стараться заблочить телегу, потому что там такое никогда не запилят (


  1. Andrey4ik
    29.08.2026 09:14

    Чел пишет хорошую умную статью но использует camelCase на питоне...