Виджеты в сообщениях — интерактивные блоки из HTML, CSS и JavaScript прямо в переписке — дошли до бет Telegram для Android и iOS. 25 октября они появились в Android 13.2.0 (7254) и iOS 13.2 (35726), и на Android самодельный виджет уже исполняет JavaScript. Сборка Android 13.2.0 (7260) от 27 октября показывает, как это устроено: без браузерного движка, на своём интерпретаторе с квотой процессора, с отдельным судьёй, который решает, было ли нажатие настоящим. Бот, которому виджет отправил запрос, узнаёт id нажавшего. В Telegram Desktop и в публичном API виджетов по-прежнему нет.

Механику запросов к боту по бете для macOS разбирал материал от 27 октября; здесь — то, что добавляет сборка для Android.

Где виджеты есть

КлиентСборкаЧто видно
macOS, бета13.1.0песочница, запросы к боту, тестовые страницы — с середины октября
Android, бета13.2.0 (7254, 7260)схема, песочница, тексты интерфейса; виджеты работают
iOS, бета13.2 (35726)виджеты в сообщениях и инструменты для их проверки
Telegram Desktop7.3.3нет: ни в коде, ни в схеме слоя 232
Bot API10.3нет

Бета Android раздаётся официальным каналом и прямой ссылкой с сайта Telegram. Протокол клиента в ней — слой 233, на один новее публичного.

Как виджет лежит в сообщении

Виджет — новый тип медиа, messageMediaWidget. Кроме самого кода (payload) и размеров в нём два набора данных: chat_data — данные этого сообщения, и public_data с хешем — общие данные виджета, которые бот может менять после отправки (messages.editMessageWidgetData, updateMessageWidgetData). Флаги can_query и background_query говорят, можно ли из виджета писать боту по нажатию и без него, is_copy помечает пересланную копию. Виджет может прийти и как инлайн-ответ бота — botInlineMessageWidget.

Разрешения ставит отправитель. В сообщениях об ошибках из нативной части клиента названы серверные флаги allow_queries и fwd_allow_queries: без первого виджет не пишет боту вовсе, без второго молчит его пересланная копия. Фоновый запрос требует ещё background_query и уходит, только пока виджет на экране.

Что получает бот

Нажатие превращается в messages.getBotWidgetQueryAnswer с полями чат, сообщение и data — до 64 байт. Бот получает обновление updateBotWidgetQuery и отвечает методом messages.setBotWidgetQueryAnswer одной строкой result.

В обновлении для бота поле user_id обязательное, без флага: бот всегда знает, кто нажал. Рядом приходят chat_type, chat_instance — тот же обезличенный идентификатор чата, что у обычных инлайн-кнопок, — номер сообщения и widget_id. Для пересланных копий и инлайн-сообщений есть свои обновления — updateBotWidgetCopyQuery и updateInlineBotWidgetQuery.

Это похоже на нажатие инлайн-кнопки с колбэком, только кнопку рисует скрипт, а данные собирает он же.

Как исполняется код виджета

В Android нет ни WebView, ни Chromium под виджетом. Код исполняется нативной библиотекой twidget внутри основной библиотеки клиента — то же имя носят типы Twidget* в бете для macOS и закрытый репозиторий, подключённый к её исходникам. Скрипты песочницы лежат в клиенте дважды: исходником и байткодом .qbc, который запускает встроенный интерпретатор JavaScript. Картинку рисует свой рендерер прямо в окно Android — браузерной вёрстки нет, есть её разбор: отдельный пакет validator проверяет HTML и CSS.

Ограничения прописаны в коде, а не в правилах для разработчиков:

  • процессор — у виджета квота времени (cpuQuotaMs), превышение заканчивается ошибкой «CPU limit exceeded»;
  • таймеры — число одновременных ограничено, лишний движок отклоняет;
  • сеть — fetch, XMLHttpRequest, WebSocket и navigator.sendBeacon заглушены, navigator.onLine всегда ложь, userAgent — TelegramWidget/1.0;
  • хранилище — каждый виджет начинает с пустого;
  • выходы наружу — openLink берёт только https-адрес до 256 символов и спрашивает: «This widget wants to open:», copyText — до 256 символов;
  • мост к клиенту — после запуска служебные функции __host, __hostOps и соседние удаляются из глобального объекта, и код виджета до них не дотягивается.

Файлы песочницы клиент сверяет с контрольными суммами из manifest.txt, расхождение — отдельная ошибка «Bundle checksum mismatch».

Как клиент отличает нажатие от имитации

Запрос к боту без фонового разрешения уходит только из обработчика нажатия — «один вызов на одно нажатие на видимом виджете», как сказано в сообщении об ошибке. Решает, было ли нажатие, не скрипт, а нативный судья касаний: функции arbiterDown, arbiterMove, arbiterUp и arbiterHoldDeadline получают координаты пальца и время удержания. По замыслу синтетический клик из JavaScript, таймер или перетаскивание запроса не отправят.

В бете есть и инструменты для проверки этой границы: экран соответствия (WidgetConformanceActivity) и прогон набора атак (attackScan, WidgetAttacks). Набор стендов-атак для macOS — 28 страниц, которые пытаются отправить сообщение без нажатия или увести по ссылке, — лежит в открытом коде клиента.

Что видит пользователь

Тексты интерфейса уже лежат в английском словаре клиента: «Loading widget…», «Widget failed to load», «Widget stopped», «Preview of the widget» и предупреждение «This widget can send data to its bot's server when you tap it.»

Отдельного переключателя, который отключал бы виджеты, в настройках беты нет — на это обращают внимание тестировщики. Отправлять виджеты пока могут не все, а в Bot API методов для них нет.