Виджеты в сообщениях — интерактивные блоки из 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 Desktop | 7.3.3 | нет: ни в коде, ни в схеме слоя 232 |
| Bot API | 10.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 методов для них нет.




