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

Это новые HTML-виджеты. На весь код дают всего 4 КБ после сжатия, и мне стало интересно, что в них вообще поместится. Оказалось, даже игра.

Для эксперимента я собрал башню: нажимаешь на сообщение, очередной этаж падает вниз. Если поставил криво, торчащий кусок отваливается. Отправил её себе в Telegram, потыкал. Вся игра заняла чуть больше 3 КБ после сжатия.

Игра с башней работает внутри сообщения Telegram

Игра с башней работает внутри сообщения Telegram

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

Одна пиццерия, три интерфейса

Давайте возьмём классический пример: пиццерия принимает заказы через Telegram. Банально, зато на нём удобно сравнить обычные сообщения бота, Mini App и новые виджеты.

Покупатель выбрал «Маргариту», решил взять две и хочет увидеть итоговую сумму.

Сообщение бота

Кнопка «Добавить в заказ» стоит под сообщением. Когда на неё нажимают, бот получает событие. Если нужно показать новую сумму, бот редактирует сообщение или присылает следующее.

Сообщение бота с ценой и кнопкой «Добавить в заказ»

Сообщение бота с ценой и кнопкой «Добавить в заказ»

Mini App

Mini App по сути открывает ваш сайт во встроенном браузере Telegram, на отдельном экране. Вы размещаете страницу на сервере, а Telegram загружает её по URL. Для нашей пиццерии это может быть каталог с корзиной и формой доставки.

У Mini Apps есть API для интеграции с Telegram: можно получить данные пользователя, подстроить цвета под тему мессенджера или использовать его кнопки. Сам интерфейс вы пишете на привычных HTML, CSS и JavaScript.

Каталог пиццерии в Mini App с переходом в корзину

Каталог пиццерии в Mini App с переходом в корзину

HTML-виджет

Жмёте «+», и сумма меняется прямо в карточке. Её пересчитывает скрипт внутри сообщения. Когда нажмёте «Оформить заказ», виджет обратится к боту, чтобы тот записал заказ на сервере.

HTML-виджет в сообщении с выбором количества и пересчётом суммы

HTML-виджет в сообщении с выбором количества и пересчётом суммы

Как меняется статус в уже отправленной карточке

Обновление с сервера я пока не пробовал, поэтому этот пример разобрал по документации Telegram. Допустим, покупатель оформил заказ. Бэкенд пиццерии сохранил его под номером 42 со статусом «Принят», и теперь бот отправляет покупателю карточку.

Карточка заказа

Карточка хранится в файле order-widget.html. В разметке есть поля для номера и статуса заказа. JavaScript внутри <script> заполняет эти поля при открытии виджета и обновляет их, когда бот присылает новые данные.

<p>Заказ №<span id="order-id">…</span></p>
<p>Статус: <strong id="order-status">Загрузка…</strong></p>

<script>
  const widget = window.TelegramWidget;
  const orderId = document.getElementById('order-id');
  const orderStatus = document.getElementById('order-status');

  function renderOrder() {
    try {
      const order = JSON.parse(widget.chatData || 'null');
      orderId.textContent = order?.id ?? '—';
      orderStatus.textContent = order?.status ?? 'Нет данных';
    } catch {
      orderStatus.textContent = 'Ошибка данных';
    }
  }

  widget.onEvent('chatDataChanged', renderOrder);
  renderOrder();
</script>

Объект window.TelegramWidget предоставляет Telegram. Его свойство chatData содержит строку с данными этого сообщения. Функция renderOrder() разбирает JSON и записывает номер заказа в order-id, а статус в order-status. В конце скрипт подписывается на событие chatDataChanged и вызывает renderOrder() первый раз, чтобы сразу показать заказ.

Бот отправляет карточку покупателю

В нашем примере бэкенд написан на Node.js. Он читает HTML из файла, а токен бота из переменной окружения TELEGRAM_BOT_TOKEN. Покупатель общается с ботом в личном чате с условным ID 123456789. Его заказ №42 уже принят.

import { readFile } from 'node:fs/promises';

const token = process.env.TELEGRAM_BOT_TOKEN;
const html = await readFile('./order-widget.html', 'utf8');

async function callTelegram(method, params) {
  const response = await fetch(
    `https://api.telegram.org/bot${token}/${method}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(params),
    },
  );
  const data = await response.json();
  if (!response.ok || !data.ok) {
    throw new Error(data.description || 'Ошибка Telegram API');
  }
  return data.result;
}

async function sendOrderWidget(chatId, order) {
  return callTelegram('sendWidget', {
    chat_id: chatId,
    html,
    chat_data: JSON.stringify(order),
  });
}

// Пример заказа, который бэкенд уже принял.
const order = { id: 42, status: 'Принят' };
const chatId = 123456789; // Условный ID чата покупателя.
const message = await sendOrderWidget(chatId, order);

Функция sendOrderWidget() отправляет в Telegram разметку в поле html и строку {"id":42,"status":"Принят"} в поле chat_data. Статус «Принят» задан в объекте order на сервере. JavaScript в виджете прочитает его через widget.chatData и покажет покупателю.

Telegram возвращает отправленное сообщение, и код сохраняет его в message. Поля message.chat.id и message.message_id указывают, где находится карточка заказа №42. По ним бот найдёт её при обновлении статуса.

Кухня начала готовить

Кухня взяла заказ в работу. Бэкенд меняет статус в order на «Готовится» и вызывает editMessageWidgetData через функцию callTelegram. ID чата и сообщения он берёт из сохранённого message.

// Позже, когда кухня взяла этот заказ в работу.
order.status = 'Готовится';

await callTelegram('editMessageWidgetData', {
  chat_id: message.chat.id,
  message_id: message.message_id,
  chat_data: JSON.stringify(order),
});

Telegram обновляет chatData у выбранного сообщения и вызывает событие chatDataChanged. Наш JavaScript снова запускает renderOrder(), и надпись меняется на «Готовится». HTML-файл повторно отправлять не нужно. Чтобы изменить сам код виджета, придётся прислать новый.

Какие ограничения есть у виджетов

Вся разметка, стили и JavaScript должны уложиться в 32 768 байт исходника и 4 096 байт после сжатия. Поэтому к карточке пиццы уже не так просто добавить фотографии. Загрузить их по URL нельзя, а встроенные в HTML изображения тоже занимают место в этих четырёх килобайтах.

JavaScript выполняется в QuickJS без доступа к сети. Например, скрипт карточки не сможет сам запросить актуальную цену через fetch. За внешними данными ему придётся обращаться к боту. Это повлияет на то, какую логику пиццерия сможет перенести в сообщение.

С вводом адреса доставки тоже возникнет проблема. Telegram удаляет из разметки input, textarea и select, а клавиатура внутри виджета недоступна. Количество пицц можно выбрать кнопками, но для ввода улицы и номера квартиры понадобится другой интерфейс. В нашем примере форму доставки можно оставить в Mini App, а в переписке показывать карточку заказа со статусом.

Где это можно попробовать

Когда башня заработала, я попытался переслать её в канал. Telegram не дал. В публичном Bot API виджеты пока работают в личке с ботом, а пересылка отключена. Если хотите потыкать свой HTML в чате, отправьте его себе через sendWidget. Подписчикам канала пока можно показать запись или дать ссылку на бота.

Мне не терпится увидеть, что люди с этим сделают. Уже хочется открывать чужие эксперименты, тыкать во всё подряд и разбираться, как это вообще уместили в одном сообщении.

Документация по виджетам · Метод sendWidget в Bot API