Анатомия Telegram Mini Apps: о разработке изнутри
2 поста
Примеры кода — на C#/.NET, потому что так написан наш прод. Но вся статья — про механику Telegram, а не про язык: то же самое собирается на Node, Python или Go без изменений. Именно ради переносимой механики статья и написана.
О чём статья? Это вторая часть серии о том, как мы вайб-кодингом построили GoosleeBot — мост из Telegram в Google Meet, Zoom и другие сервисы видеовстреч. Здесь — самая недокументированная часть разработки под Telegram: механизм сессий, который связывает чат, miniapp и внешний браузер в одно приложение. Вот что внутри:
как передать параметры из инлайн-бота в miniapp, если кнопка в чате одна на всех участников;
как аутентифицировать пользователя miniapp за один запрос — с готовым скелетом валидации initData;
как сохранить состояние там, где переменные Telegram не работают вовсе — во внешнем браузере;
как доставлять изменения из чата в miniapp мгновенно (спойлер: WebSocket в telegram-webview работает);
как редактировать сообщения в чате асинхронной очередью и не словить бан от Bot API;
и другие.
А это вся серия:
Часть 1 — история продукта и честный вердикт Telegram как платформе: правила для памяти агента, функция «где я?», туннели.
Часть 2 (вы здесь) — механизм сессий: параметры, аутентификация, обратная связь, редактирование без бана.
Часть 3 — UX-хаки: бот, который «звонит» как телефон, онбординг-воронка одним JSON-файлом и почему ссылки надо готовить до клика.
Наш главный сценарий выглядит невинно: в чате лежит сообщение бота с кнопкой «Присоединиться к звонку». Но у этой кнопки три неприятных свойства, о которых не пишут в туториалах:
Первое: кнопка одна на всех. Сообщение в чате общее — его видят все участники. Нажать может любой, и для каждого miniapp должен открыться со «своим» контекстом: кто ты, в каком звонке, что тебе можно. А кнопка умеет ровно одно — открыть ссылку. Одинаковую для всех.
Второе: нужна аутентификация и обратная связь. Мало открыть страницу — надо понять, кто её открыл (и не поверить на слово), а потом вернуть результат действий обратно: в сообщение в чате, другим участникам, в их miniapp.
Третье, самое коварное: часть флоу уходит во внешний браузер. OAuth-авторизация у Google или Zoom по правилам провайдеров происходит в системном браузере — и в момент, когда пользователь туда перешёл, все переменные miniapp перестают существовать. Нет `Telegram.WebApp`, нет initData, нет ничего. Из всего контекста Telegram остаётся ровно то, что вы сами успели положить в URL.
Три проблемы — одно решение: серверные сессии. И сразу важная рамка: это не трюк для проброса параметров. Это полноценный аналог классической веб-сессии — серверный объект с временем жизни и типизированным хранилищем значений, на котором держится вся серверная логика. Если в вашем продукте есть хотя бы сценарий «выйти из miniapp наружу и вернуться» — механизм сессий обязателен, без вариантов.
Скелет предельно простой (напоминаю, здесь C#, но это словарь с TTL — он есть в любом языке):
```csharp
public sealed class Session
{
public string Key { get; init; } // Guid без дефисов — едет в URL
public SessionType SessionType { get; set; } // RequestCall | MiniAppRoute | AccessToken | ...
public TimeSpan SessionLifeTime { get; set; }
public bool IsExpired => UtcNow - CreatedDate > SessionLifeTime;
public T? GetData<T>() where T : class { ... } // типизированные данные сценария
public void SetData<T>(T? value) where T : class { ... }
}
```
Хранилище — обычный `ConcurrentDictionary<string, Session>` в памяти процесса, фоновый воркер выметает протухшие. Ключ — случайный Guid: он непредсказуем, поэтому сам по себе работает как секрет.
Главное здесь — `GetData<T>()`. У каждого типа сессии свой класс данных, и это данные не «для передачи», а рабочее состояние, на которое опирается серверная логика. Например, у сессии инлайн-звонка внутри лежит: кто создал звонок, список пользователей, нажимавших кнопки, выбранный провайдер, статус звонка, id инлайн-сообщения в чате, последний отрисованный текст этого сообщения. Сервер читает и меняет это состояние на каждом шаге сценария — ровно как классическая сессия в вебе, просто ключ к ней приезжает не в cookie (помните правило из части 1: cookies в webview ненадёжны), а в URL и токенах.
Теперь само решение — цепочка из трёх сессий. Звучит громоздко, на деле — три словарных записи и два редиректа:
```text
[чат] RequestCall-сессия ─── общая, одна на звонок
│ ключ уезжает в кнопку: t.me/MyBot/app?startapp= {key}
▼
[miniapp] роутер-страница: собирает Telegram.WebApp.initData → POST /init
│ сервер валидирует подпись, находит RequestCall по ключу
▼
[сервер] AccessToken-сессия ── персональная, у каждого пользователя своя,
│ внутри ссылка на общую OriginalSession
▼
[страница] редирект на целевую страницу с персональным токеном в URL
```
По шагам:
1. Создание. Когда пользователь вызывает бота в чате, сервер создаёт `RequestCall`-сессию — общую для этого звонка. Ключ уходит в кнопки. Для callback-кнопок мы кодируем его прямо в payload (`{key}@@@{команда}`), для кнопки miniapp — в параметр `startapp` диплинка. Это единственный канал: Telegram не даст кнопке передать ничего, кроме этой строки.
2. Роутер. Кнопка открывает не целевую страницу, а лёгкую страницу-роутер. Её JS собирает `initData` и шлёт на сервер:
```js
const tg = window.Telegram.WebApp;
const resp = await fetch('/miniapp/init', {
method: 'POST',
body: JSON.stringify({
initData: tg.initData, // подписанная строка — для проверки
startParam: tg.initDataUnsafe.start_param, // ключ RequestCall-сессии
platform: tg.platform, // помните «где я?» из части 1
}),
});
const { redirectUrl } = await resp.json();
location.replace(redirectUrl);
```
3. Проверка и обмен. Сервер валидирует подпись initData (следующий раздел), находит общую сессию по ключу из `start_param` — и выпускает персональную `AccessToken`-сессию: «пользователь X в контексте звонка Y». Внутри неё — ссылка на общую:
```csharp
var accessSession = SessionService.Create(SessionType.AccessToken, lifeTime);
accessSession.SetData(new AccessTokenSessionData {
UserId = user.Id,
OriginalSession = requestCallSession, // общий контекст звонка — один на всех
});
return RedirectUrlFor(page, accessSession.Key); // токен — в URL целевой страницы
```
Одна общая сессия звонка — много персональных токенов поверх неё. Любой запрос с целевой страницы несёт персональный токен, и сервер за один lookup знает и «кто», и «в каком звонке». Проблема «кнопка одна на всех» решена.
4. Внешний браузер. А теперь то, ради чего это всё. Когда пользователь уходит на OAuth к Google и возвращается на наш callback-URL — он приходит в обычном браузере, без единой переменной Telegram. Но токен-то мы положили в URL заранее. Сервер достаёт по нему AccessToken-сессию → из неё OriginalSession → и продолжает сценарий, как будто никто никуда не уходил. Состояние пережило переход чат → miniapp → внешний браузер → обратно, потому что оно никогда и не покидало сервер.
Telegram кладёт в miniapp данные пользователя двумя способами: `initDataUnsafe` (удобный распарсенный объект) и `initData` (сырая строка с криптографической подписью). Слово «Unsafe» в названии — не кокетство: эти данные каждый умеет подделать, отправив вам любой user_id. Использовать их для UI — можно, для серверных решений — только после проверки подписи сырой строки.
Алгоритм проверки описан в доках Telegram, вот его скелет целиком — это один статический метод:
```csharp
public static bool Validate(string initData, string botToken)
{
var parsed = ParseQuery(initData); // initData — это query string
var receivedHash = parsed["hash"];
var dataCheckString = string.Join('\n', // все поля кроме hash,
parsed.Where(kv => kv.Key != "hash") // отсортированы по ключу
.OrderBy(kv => kv.Key, StringComparer.Ordinal)
.Select(kv => $"{kv.Key}={kv.Value}"));
var secretKey = HMACSHA256(key: "WebAppData", message: botToken);
var expected = HMACSHA256(key: secretKey, message: dataCheckString);
if (!FixedTimeEquals(expected, receivedHash)) return false; // константное время!
return UtcNow - FromUnix(parsed["auth_date"]) < MaxAge; // и свежесть: у нас 1 час
}
```
Три детали, которые агент при вайб-кодинге стабильно упускает — проверьте руками:
Сортировка полей ordinal, не culture-зависимая — иначе подпись «иногда не сходится».
Сравнение хэшей в константное время (`FixedTimeEquals`), а не `==` — защита от timing-атак на подбор подписи.
Проверка `auth_date`. Подпись без срока годности — это вечный пропуск: перехваченная один раз строка initData работала бы годами.
После валидации `user_id` из initData можно считать доказанным — Telegram расписался. Заметьте, что получилось: регистрация, логин и восстановление пароля в вашем приложении не существуют как задачи. Это одна из главных причин строить бизнес-приложения на Telegram вообще.
Да, наши сессии — in-memory, `ConcurrentDictionary`, без Redis. Это осознанный трейдофф, и вот его границы:
Когда это нормально: один инстанс приложения; сессии короткоживущие (минуты-часы) и по природе восстановимые — пользователь в худшем случае нажмёт кнопку ещё раз; критичное для бизнеса состояние (звонки, пользователи, настройки) и так живёт в PostgreSQL, сессия лишь оркестрирует сценарий.
Когда сломается: рестарт или деплой обнуляет живые сценарии (у нас это «кнопка перестала отвечать, вызовите меню заново»); второй инстанс за балансировщиком не увидит чужих сессий — привет, sticky sessions или внешний стор.
Для вайб-кодера мораль такая: начинайте со словаря в памяти — это ноль инфраструктуры и вся механика статьи работает как есть. Но заведите интерфейс хранилища сессий с первого дня, чтобы Redis, когда понадобится, был заменой одного класса, а не переписыванием.
Осталась последняя стрелка: изменения должны лететь обратно. Собеседник выбрал провайдера в своём miniapp — у вас на странице должен обновиться статус; кто-то подключился к встрече — сообщение в чате должно это показать.
В части 1 я уже спойлерил: SignalR (WebSocket) внутри telegram-webview работает штатно на всех платформах. Схема такая:
При открытии страницы клиент вступает в группу по ключу общей сессии звонка: все участники одного звонка — в одной комнате.
Когда серверное состояние меняется, сервер шлёт в группу голое событие `StateChanged` — без данных.
Клиент, получив сигнал, сам запрашивает актуальное состояние обычным HTTP-запросом со своим персональным токеном.
```csharp
// сервер: состояние изменилось → пнуть группу (с debounce, см. ниже)
await hub.Clients.Group(callSessionKey).SendAsync("StateChanged");
// клиент: сигнал — не данные, а приглашение спросить
connection.on('StateChanged', () => refreshState()); // GET со своим токеном
```
Почему сигнал пустой? Три причины: не надо думать о правах в push-канале (каждый забирает состояние своим токеном — сервер сам решит, что ему видно); потерянный сигнал ничего не ломает (следующий `refreshState` всё выровняет); и события можно дебаунсить — при шквале изменений группа получает один пинок в N миллисекунд, а не очередь устаревших снапшотов. Плюс дешёвая страховка: редкий фоновый poll на случай, если WebSocket всё-таки умер.
Обратная связь долетает не только до miniapp, но и до того самого инлайн-сообщения в чате: «Готово. Подключились: Аня, Дмитрий». И вот здесь вайб-кодеры массово наступают на грабли: наивный код дёргает `editMessageText` на каждое изменение состояния. Два участника нажали кнопки одновременно — два edit'а; шквал событий — шквал edit'ов. А у Bot API на это две реакции: ошибка `400: message is not modified` (текст совпал с текущим) и flood-лимиты вплоть до временной блокировки бота — для продукта, который живёт в чужих чатах, это смерть.
Решение — не редактировать сообщение из бизнес-логики вообще. Вместо этого:
```text
логика: «состояние сессии X изменилось» → пометка X в очереди (дедупликация по ключу)
воркер: берёт ключ → строит текст и клавиатуру ИЗ ТЕКУЩЕГО состояния сессии
→ сравнивает с последним отправленным текстом (он хранится в сессии!)
→ совпало: молча пропустить | изменилось: editMessageText, запомнить текст
```
Три свойства, ради которых всё затевалось: дедупликация — десять изменений за секунду дают одно редактирование, потому что пометки схлопываются по ключу сессии, а текст строится по финальному состоянию; идемпотентность — сравнение с `LastInlineMessageText` из сессии гарантирует, что мы никогда не пошлём Telegram то, что у него уже есть (заметьте: это ещё одна работа для серверного состояния сессии); асинхронность — бизнес-логика не ждёт Telegram API и не падает от его ошибок.
Правило в память агента: сообщение в чате — это проекция состояния сессии, обновляемая фоновым воркером. Бизнес-логика сообщений не трогает.
Механика из этой статьи — переиспользуемый каркас для любого бизнес-приложения на miniapp, где есть «общая кнопка» и «выход наружу»: серверная сессия с типизированным состоянием; персональные токены поверх общего контекста; подпись initData как замена всей системе логина; пустые сигналы по WebSocket вместо данных в push; и очередь-проекция для сообщений в чате. Ни один из этих элементов не завязан на C# — это протокол работы с платформой.
В части 3 — фокусы: как бот «звонит» пользователю так, что телефон ведёт себя как при настоящем входящем вызове (спойлер: удаляем и переотправляем сообщение — и почему это работает). Почему в Telegram невозможен привычный веб-паттерн «клик → сервер сформировал ссылку → редирект» и как жить с тем, что конечный URL должен лежать в кнопке до клика. И скрипт-чат: онбординг-воронка целиком в одном JSON-файле — с эффектом печати, слайдами и аналитикой по шагам.
Всё описанное написано для Telegram, но в большей части справедливо и для MAX — механика ботов, miniapp и сессий там строится по тем же принципам. Если есть желание сделать адаптацию под MAX и российские сервисы видеовстреч — велком в личку.
Потрогать сам мост: @GoosleeBot · gooslibot.com
Примеры кода в серии — на C#/.NET, потому что так написан наш прод. Но всё описанное — про механику Telegram, а не про язык: то же самое собирается на Node, Python или Go без изменений. Собственно, ради этой переносимой механики статья и написана.
О чём статья? Мы вайб-кодингом построили мост из Telegram в Google Meet, Zoom и другие сервисы видеовстреч — и набили все шишки, которые можно набить о miniapp при разработке бизнес-приложения. В этой статье — четыре реальных лайфхака, которые стопроцентно пригодятся вам при написании любого бизнес-приложения на Telegram miniapp. Вот некоторые из них:
готовый блок правил для памяти агента — ограничения веб-движка Telegram плюс требования каталогов (копируется в CLAUDE.md как есть);
функция «где я?» — платформа и режим, без которой каждая страница будет полем чудес;
туннель с первого дня — приём, который превращает деплой-на-каждый-чих в итерации по секундам;
как хранить состояние между вызовами бота, miniapp и внешним браузером;
бот, который «звонит» как телефон;
и другие;
А это вся серия:
Часть 1 (вы здесь) — история продукта и честный вердикт Telegram как платформе для бизнес-приложений: что дёшево, что больно, что прописать агенту до первой строчки кода.
Часть 2 — механизм сессий: как передать параметры из инлайн-бота в miniapp, а потом прочитать их во внешнем браузере; аутентификация, обратная связь в чат и как не словить бан за редактирование сообщений.
Часть 3 — UX-хаки: бот, который «звонит» как телефон, онбординг-воронка одним JSON-файлом и почему ссылки надо готовить до клика.
Звонки в Telegram работают — но именно как звонки: голос, видео, положил трубку. Встречи — другой жанр, и он в Telegram почти не развит. А вот в Google Meet и Zoom вокруг встречи выросла целая экосистема: транскрибация, AI-саммари, запись, расширения, интеграции с календарями и CRM. Когда разговор рабочий — хочется именно встречу, со всем этим обвесом.
Но переписка-то живёт в Telegram: там группа с коллегами, там клиент, которому «удобнее в телеге». И каждый раз, когда переписка дозревает до «давай голосом», начинается ритуал: открыть Google Meet или Zoom, создать встречу, скопировать ссылку, вернуться в чат, вставить, объяснить, куда нажимать.
Идея была наглая в своей простоте: пусть ссылку создаёт бот. Прямо в чате, одной кнопкой, в том сервисе, который доступен и у меня, и у собеседника. Так появился GoosleeBot — мост из Telegram в мир видеовстреч: Google Meet, Zoom, Teams, Jitsi, FaceTime и далее по списку.
Дальше был чистый вайб-кодинг: агент, промпты, итерации. Цифры такие: первый MVP — Google Meet, Zoom и Jitsi — был готов за два дня и честно доказал, что идея работает. А потом были четыре недели доводки. И вот что важно: главным расходом времени был не код — код агент пишет быстро. Время съело множество сценариев, которые пришлось охватить и протестировать: два собеседника или группа, первый звонок или сотый, подключён ли у автора провайдер, iOS или десктоп, свой чат или чужой — и каждую комбинацию надо прогнать руками в настоящем Telegram.
Именно поэтому статья устроена не как «смотрите, какие мы молодцы», а как список вещей, которые я бы прописал агенту в память до первой строчки кода. Потому что главный урок такой: Telegram — отличная платформа для продуктивити-приложений, но её веб-движок — не браузер, а Bot API — не REST-бэкенд. Если агент этого не знает, он уверенно нагенерит красивое неработающее.




Сценарий: в любом чате вызываешь бота, жмёшь кнопку — бот создаёт комнату у доступного провайдера и кладёт в чат кнопку «Присоединиться». Если собеседник не реагирует — бот умеет «позвонить»: слать сообщение-звонок с повтором, как настоящий входящий вызов (как это сделано — в третьей статье серии, там простой и наглый трюк).
Ни один участник не устанавливает ничего нового. В этом вся ставка: дистрибуция через чат — фича Telegram, которой нет ни у одной «нормальной» платформы.
Это центральная часть статьи. Если вы вайб-кодите под Telegram — скопируйте этот блок в CLAUDE.md / system prompt агента как есть. Каждое правило оплачено часами отладки «а почему на айфоне не так».
Никогда не использовать `100vh`. Только `viewportStableHeight` / CSS-переменную `var(--tg-viewport-height)` и подписку на событие `viewportChanged`. Что сломается: на iOS низ страницы уедет под панель, на Android клавиатура «сожмёт» экран и разложит вёрстку.
Никаких `window.open` и `target="\_blank"`. Только `Telegram.WebApp.openLink()` для внешних ссылок и `openTelegramLink()` для внутренних. Что сломается: на части клиентов клик молча не сделает ничего.
Никаких `alert` / `confirm` / `prompt`. Только `showAlert` / `showConfirm` / `showPopup`. Что сломается: то же самое — молчание вместо диалога, причём не везде, а «у некоторых пользователей», что хуже.
Не полагаться на cookies для авторизации в miniapp. Webview теряет их непредсказуемо. Авторизация — токеном, переданным явно (заголовок или параметр). Что сломается: пользователь «разлогинивается» между открытиями, а вы неделю ищете несуществующий баг на сервере.
`localStorage` — это кэш, а не хранилище. Всё важное — на сервере (или в `CloudStorage`). Что сломается: состояние пользователя внезапно обнулится, и вы об этом не узнаете.
Навигация «назад» — через `BackButton` Telegram, а не через history браузера. Что сломается: системная кнопка «назад» на Android закроет miniapp целиком вместо возврата на шаг.
Первым делом на каждой странице — определить, где ты: платформа и режим (miniapp или обычный браузер). От этого ветвятся CSS и поведение. Функция ниже.
Всё, по чему пользователь кликает, должно быть готово до клика. Привычная веб-схема «клик → сервер обработал → сформировал URL → редирект» в Telegram не работает: переадресации нет. Конечный URL должен лежать в кнопке уже в момент её отрисовки — то есть всю «нагрузку редиректа» надо вычислить заранее (подробный разбор — в третьей статье).
Тестировать минимум на трёх клиентах: iOS, Android, Desktop. Это три разных webview с разным рендером и разным поведением. «Работает у меня на десктопе» — это ноль из трёх.
У каталогов (и у самого Telegram, и у витрин miniapp-ов) есть требования к приложению. Переделывать под них готовый продукт — самая дорогая работа в проекте. Поэтому в память агента, жёстко:
Мультиязычность с первого экрана. Ни одной строки текста в разметке — всё через локаль-файлы. Язык пользователя приходит сам: `initDataUnsafe.user.language\_code`. Добавить второй язык в проект, где строки зашиты в вёрстку, — это переписать проект
Тема — только через `themeParams`. Все цвета — из CSS-переменных `--tg-theme-\*`, тёмная и светлая темы обязательны, подписка на `themeChanged`. Ни одного захардкоженного цвета. Что сломается: белый текст на белом фоне у половины аудитории — и отказ каталога.
Каталог проверяет: языки, обе темы, все платформы. Это требования релиза, а не полировка. Агент должен считать их частью definition of done каждой страницы.
Одна и та же страница у нас открывается на iOS, Android, десктопе — и иногда просто в браузере (пользователь скопировал ссылку). CSS и поведение различаются во всех четырёх случаях, поэтому первым в проекте появился вот такой помощник:
```js
function whereAmI() {
const tg = window.Telegram?.WebApp;
const inMiniApp = !!tg \&\& tg.initData !== ''; // пустой initData = обычный браузер
const platform = tg?.platform ?? 'browser'; // ios | android | tdesktop | macos | weba | ...
const isMobile = platform === 'ios' || platform === 'android';
return { inMiniApp, platform, isMobile };
}
// дальше — класс на body и ветвление поведения
document.body.classList.add(`plt-${platform}`, inMiniApp ? 'in-tg' : 'in-browser');
```
Где это стреляет на практике:
CSS. Отступы под шапку/низ, safe area на iOS, реакция на клавиатуру на Android — всё вешается на классы `plt-\*`.
Редиректы. После OAuth-авторизации у Google мобильного пользователя надо вернуть в Telegram диплинком `tg://`, а десктопного — обычным `https`-редиректом на страницу. Перепутаешь — получишь либо зависший браузер, либо выброшенного из миниаппа пользователя.
Заглушка для «не-Telegram». Если `initData` пуст — это не miniapp, и вместо приложения показываем страницу «откройте через бота».
Мелочь? Мелочь. Но без неё каждая страница превращается в поле чудес.
Telegram не умеет «localhost»: и webhook бота, и URL miniapp-а должны быть публичными HTTPS-адресами. Поэтому первым инструментом в проекте — раньше базы, раньше CI — должен стать туннель: ngrok, cloudflared, dev tunnels в Visual Studio — что угодно, дающее постоянный публичный адрес на вашу локальную машину.
Рабочий цикл получается такой: бот и miniapp настроены на адрес туннеля, код правится локально с hot reload, а проверяешь — в настоящем Telegram на настоящем телефоне, сразу. Без туннеля каждая итерация — это деплой; с туннелем — секунды. Помните про количество сценариев, которое съело четыре недели? Без туннеля это были бы не недели.
Дистрибуция. Продукт распространяется пересылкой кнопки в чат. Ноль установок, ноль лендингов на старте: собеседник видит кнопку — собеседник уже пользователь.
Аутентификация из коробки. Telegram сам подписывает данные пользователя и отдаёт их miniapp-у. Не нужно ни регистрации, ни паролей, ни «войти через Google» — нужно только правильно проверить подпись (об этом — вся вторая статья).
Inline-механика. Бот работает в любом чате, даже там, где его нет в участниках. Это и есть «мост»: продукт живёт внутри чужих разговоров, а не ждёт пользователей у себя.
Realtime — работает, и его нужно делать. Мы сомневались, взлетит ли SignalR (WebSocket) внутри telegram-webview. Взлетел: полноценный хаб с группами живёт в бою на всех платформах. Обновление статуса звонка прилетает в miniapp мгновенно, без перезагрузок. Fallback на polling мы держим — но как страховку, а не как основной канал. Если ваш агент предлагает «давай просто поллинг каждую секунду» — не соглашайтесь, соединение честно работает.
Зоопарк клиентов. iOS, Android, Desktop, web — четыре разных Telegram с разным webview. Отсюда правило №9 и функция «где я?».
Веб-движок с характером. Весь блок А выше — это его портрет.
Требования каталогов. Языки и темы — не опция. Кто узнаёт об этом в конце разработки, тот переписывает вёрстку.
Из скучного: приём апдейтов от Telegram у нас переключается флагом между webhook и polling — удобно для отладки, и это всё, что стоит знать об этой теме.
Итого: если ваш продукт — про коммуникацию, координацию или «сделать что-то не выходя из чата», Telegram даёт фору любой платформе. Плата за это — дисциплина по отношению к webview. Дисциплину можно делегировать: просто отдайте агенту правила из этой статьи.
Часть 2 — про самое неочевидное. Инлайн-кнопка в чате — одна на всех участников, и открывает она просто ссылку. Как передать в miniapp параметры конкретного звонка? Как понять, кто именно нажал, и не дать себя обмануть? Как вернуть ответ из miniapp обратно в чат? И отдельная ловушка: часть флоу неизбежно уходит во внешний браузер (например, OAuth-авторизация у Google) — а там переменные miniapp не работают вовсе, и из контекста Telegram остаётся только то, что вы сами положили в URL. Расскажу про механизм сессий, который закрывает всё это разом — причём это не трюк для проброса параметров, а полноценный серверный аналог сессии: типизированное хранилище значений, на котором держится вся серверная логика звонка. Бонус: как редактировать сообщения в чате асинхронной очередью и не словить бан от Bot API.
Часть 3 — про фокусы. Как заставить бота «звонить» — с повторными уведомлениями, как настоящий входящий вызов (спойлер: удаляем и переотправляем сообщение). Почему ссылки нужно готовить заранее — и это не про классическое «пре-генерите URL»: в обычном вебе вы кликаете, сервер обрабатывает запрос, формирует ссылку и делает переадресацию — в Telegram эта схема не работает в принципе, редиректа нет, и всю «нагрузку редиректа» приходится вычислять заранее, до клика. Как провести пользователя по продукту JSON-файлом вместо онбординг-движка: скрипт-чат, который печатает, показывает слайды и пишет воронку в аналитику.
Всё описанное написано для Telegram, но в большей части справедливо и для MAX — механика ботов, miniapp и сессий там строится по тем же принципам. Если есть желание сделать адаптацию под MAX и российские сервисы видеовстреч — велком в личку.
Потрогать сам мост: @GoosleeBot · gooslibot.com
