1

Бот, который звонит: дешёвые UX-хаки для Telegram-бота⁠⁠

Серия Анатомия Telegram Mini Apps: о разработке изнутри

Примеры кода — на C#/.NET, потому что так написан наш прод. Но вся статья — про механику Telegram, а не про язык: то же самое собирается на Node, Python или Go без изменений. Именно ради переносимой механики статья и написана.

О чём статья? Это третья часть серии о том, как мы вайб-кодингом построили GoosleeBot — мост из Telegram в Google Meet, Zoom и другие сервисы видеовстреч. Первые две части были про фундамент; эта — про фокусы: UX-приёмы, которые выглядят как магия, а стоят как пара вечеров. Вот что внутри:

  • как заставить бота «звонить» — чтобы телефон пользователя вёл себя как при настоящем входящем вызове, хотя Bot API звонков не умеет в принципе;

  • почему в Telegram невозможен привычный веб-паттерн «клик → сервер сформировал ссылку → редирект» — и как проектировать кнопки, когда переадресации нет;

  • как сделать онбординг-воронку целиком в одном JSON-файле — с эффектом печати, слайдами и аналитикой по шагам;

  • как один интерфейс провайдера держит шесть сервисов видеовстреч — от OAuth-API до голых диплинков;

  • и другие.

А это вся серия:

  • Часть 1 — история продукта и честный вердикт Telegram как платформе: правила для памяти агента, функция «где я?», туннели.

  • Часть 2 — механизм сессий: параметры, аутентификация, обратная связь, редактирование без бана.

  • Часть 3 (вы здесь) — UX-хаки: бот, который «звонит», ссылки до клика, скрипт-чат-воронка.

Хак 1. Бот, который звонит

Проблема. Наш продукт — про звонки, а Bot API звонить не умеет вообще: бот может только отправить сообщение. Но «я создал встречу, а собеседник не заметил» — главный убийца сценария. Сообщение со ссылкой на встречу — это письмо. А нужен звонок: настойчивый, повторяющийся, который вибрирует в кармане, пока не возьмёшь трубку.

Решение. Телефон реагирует не на «звонок» — телефон реагирует на уведомление. Значит, надо, чтобы уведомления приходили снова и снова, как гудки. Трюк: бот отправляет сообщение «📞 Вам звонит Дмитрий» с кнопками «Принять» / «Отклонить», ждёт несколько секунд — удаляет его и отправляет заново. Каждая переотправка — новый пуш: телефон снова вибрирует и светится. С точки зрения пользователя это неотличимо от дозвона; с точки зрения чата — там всегда одно аккуратное сообщение, а не лесенка из десяти одинаковых.

Вся механика — один фоновый сервис-планировщик. По шагам:

создать задание «прозвон» по ключу сессии звонка (идемпотентно: второй запрос — no-op)

получатели = участники чата, кроме инициатора, у кого разрешён прозвон

цикл до N попыток с интервалом T:

удалить прошлое сообщение-звонок (если было)

отправить новое: «Вам звонит X» [Принять — URL] [Отклонить — callback]

стоп-условия (проверяются каждый тик):

участник подключился к встрече / нажал «Отклонить»

звонок завершён или отменён / сессия истекла

после остановки: удалить все висячие сообщения-звонки

Краевые случаи, которые превращают трюк в фичу (и которые агент сам не сделает — диктуйте):

  • Идемпотентность. Нетерпеливый инициатор жмёт «Позвонить» три раза — задание должно создаться одно. Ключ задания = ключ сессии звонка (та самая сессия из части 2).

  • Стоп по подключению. Сервис следит за состоянием звонка: собеседник вошёл во встречу — гудки мгновенно прекращаются, даже если он так и не нажал «Принять» в чате, а зашёл по ссылке из сообщения.

  • Уборка. После остановки — удалить все оставшиеся сообщения-звонки. «Пропущенный вызов» не должен висеть в чате мусором.

  • Cooldown. Повторный прозвон того же звонка — не раньше, чем через паузу. Иначе кнопка «Позвонить» превращается в оружие спама, а ваш бот — в кандидата на блокировки.

  • Согласие. У получателя должен быть флаг «можно ли мне звонить». Незапрошенные повторные пуши — это то, за что боты банят люди, а не Telegram.

Правило в память агента: «звонок» бота = цикл «удалить и переотправить» с жёсткими стоп-условиями и уборкой за собой.

Хак 2. Ссылки готовят до клика, потому что редиректа не будет

Это самый контринтуитивный урок серии, поэтому разберу медленно.

Как вы привыкли в вебе: пользователь кликает → запрос приходит на сервер → сервер обрабатывает (создаёт встречу, выписывает токен, выбирает провайдера) → формирует конечный URL → отвечает переадресацией → браузер уезжает. Клик запускает вычисление, ссылка — его результат.

Как в Telegram: этой схемы не существует. URL-кнопка в сообщении — это готовая ссылка, зашитая в момент отправки сообщения. Между «пользователь нажал» и «клиент открыл URL» нет вашего сервера — Telegram сам мгновенно открывает то, что лежит в кнопке. Некуда встроить «обработать и переадресовать».

Следствие: всю «нагрузку редиректа» приходится выполнять заранее — до того, как пользователь нажмёт, и даже до того, как он увидит кнопку. Кнопка «Принять» в сообщении-звонке уже содержит персональную ссылку на страницу подключения; она была вычислена и положена в сессию в момент, когда бот формировал сообщение. У нас это выглядит так:

```csharp

// при создании сообщения-звонка, ДО отправки:

var prepareUrl = BuildPrepareCallUrl(callSession, recipient); // вся логика — здесь

callData.PrepareCallUrl = prepareUrl; // и в состояние сессии

var keyboard = new InlineKeyboard(

UrlButton(t("Принять"), prepareUrl), // готовый конечный URL

CallbackButton(t("Отклонить"), $"{callSession.Key}@@@decline"));

```

Что это меняет в проектировании — три привычки на замену:

  • Думайте «какие переходы возможны с этого экрана» вместо «что сделать по клику». Каждая кнопка = заранее просчитанный маршрут. Если маршрут зависит от того, кто нажмёт (а кнопка в чате одна на всех — привет, часть 2), — в кнопке лежит URL страницы-роутера, которая разберётся по сессии и токену уже на своей стороне.

  • Отделяйте «дорогое» от «мгновенного». Создание встречи у провайдера — секунды; их нельзя прятать за клик по URL-кнопке. Либо создавайте заранее, либо ведите на промежуточную страницу «Соединяем…», которая честно показывает процесс (у нас — второй вариант, со статусами по WebSocket из части 2).

  • То же касается возвратов. Ссылка «вернуться в Telegram» из внешнего браузера (`tg://`-диплинк после OAuth) — тоже готовится заранее и зависит от платформы (функция «где я?» из части 1).

Правило в память агента: в Telegram ссылка — не результат клика, а его предусловие. Всё кликабельное должно содержать конечный URL уже в момент отрисовки.

Хак 3. Онбординг-воронка в одном JSON-файле

Проблема. Пользователь зарегистрировался — и его надо провести за руку: что это, зачем, как сделать первый звонок. Классические варианты дороги: цепочка сообщений от бота — это state machine в коде и вечные правки «поменяйте текст третьего шага»; полноценный тур в miniapp — фронтенд-работа на недели.

Решение. Онбординг — это чат. Так пусть он и будет чатом — только скриптованным. Страница в miniapp рендерит «переписку с ботом», а весь сценарий лежит в одном JSON-файле: узлы-сообщения, кнопки-переходы, слайды. Вот реальный кусок нашей воронки (сокращённо):

```json

{

"chat-intro": {

"chatvelocity": 66, // скорость «печати», символов в секунду

"messages": [

{

"name": "intro",

"text": "<b>Привет!</b> Я GoosleeBot — звонки через Meet, Zoom… прямо из чата.",

"buttons": [

{ "key": "see_examples", "text": "💡 Примеры ситуаций", "goto": "examples" },

{ "key": "how_it_works", "text": "❔ Как это работает", "goto": "how" },

{ "key": "finish", "text": "Вернуться в Telegram", "href": "@return_to_bot" }

]

},

{

"name": "how_to_call",

"text": "Тапаете кнопку в чате — бот создаёт звонок…",

"slides": [ "step1.png", "step2.png", "step3.png" ] // карусель со скриншотами

}

]

}

}

```

Фронт один на все сценарии: печатает текст с заданной скоростью, показывает карусель слайдов, рисует кнопки. `goto` — переход к узлу графа, `href` — внешняя ссылка, служебный `@return_to_bot` закрывает miniapp и возвращает в чат. Каждое нажатие улетает на сервер как событие аналитики — и вот вам воронка по шагам бесплатно: докуда доходят, где отваливаются, какая кнопка мертва.

Скрипт-чат в действии

Скрипт-чат в действии

Почему это дешёво и почему это стоит красть:

  • Новый сценарий = новый JSON. Помощь, страница настроек, промо новой фичи — без единой строчки кода. Правки текстов — правки файла, их может делать не-программист (или агент — идеальная задача для вайб-кодинга: «добавь в воронку шаг про групповые звонки»).

  • Локализация — файлы по локалям (`chats_ru.json`, `chats_en.json`) с fallback на дефолтную. Требование каталогов из части 1 закрывается само.

  • Это и есть ваша презентация продукта. Тот же движок, который онбордит новичка, показывает «что нового» старым пользователям и продаёт функции — воронка и витрина в одном механизме.

Вместо эпилога: как один интерфейс держит шесть провайдеров

Обещанная последняя тема — коротко, потому что после трёх статей она почти очевидна. «Мост» держится на одном интерфейсе:

```csharp

public interface ICallProvider

{

CallProviderKind Kind { get; }

bool DirectLinkCall { get; } // ссылку можно собрать без API?

Task<ProviderResult> CreateAsync(CallContext ctx); // → ссылка на встречу

}

```

А адаптеры под ним — трёх сортов, по убыванию сложности:

  1. OAuth-API (Google Meet, Zoom): полноценная интеграция — авторизация пользователя, хранение и рефреш токенов, создание встречи через API провайдера. Самые дорогие и самые функциональные.

  2. Генерация комнаты (Jitsi): ни аккаунта, ни API — ссылка со случайным именем комнаты, комната возникает при первом входе. Адаптер на двадцать строк.

  3. Диплинк (FaceTime, Teams): просто правильно собранная ссылка на чужое приложение.

Поверх — автовыбор: если у автора настроен любимый провайдер — он; нет — первый доступный по приоритету. Пользователь нажал одну кнопку, а какой из шести мостов под ней сработал — деталь реализации. Для вайб-кодера главный вывод: начинайте с адаптера третьего сорта. Наш MVP за два дня (из части 1) был возможен ровно потому, что Jitsi-адаптер — это генерация строки; OAuth-сложность Meet и Zoom доехала позже, когда идея уже была доказана.

Итог серии

Три статьи — один тезис: Telegram даёт бизнес-приложению неприличную фору (дистрибуция через чат, аутентификация без регистрации, realtime из коробки), но взамен требует выучить его правила: webview — не браузер, кнопка — не запрос, ссылка — не результат клика, а сообщение в чате — проекция серверного состояния. Выучите их сами — или просто продиктуйте своему агенту: все правила серии сформулированы так, чтобы копироваться в CLAUDE.md как есть.


Всё описанное написано для Telegram, но в большей части справедливо и для MAX — механика ботов, miniapp и сессий там строится по тем же принципам. Если есть желание сделать адаптацию под MAX и российские сервисы видеовстреч — велком в личку.

Потрогать сам мост: @GoosleeBot · gooslibot.com

Лига программистов

2.4K пост12K подписчиков

Правила сообщества

- Будьте взаимовежливы, аргументируйте критику

- Приветствуются любые посты по тематике программирования

- Если ваш пост содержит ссылки на внешние ресурсы - он должен быть самодостаточным. Вариации на тему "далее читайте в моей телеге" будут удаляться из сообщества

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества

Недвижимость и ремонт

Теги

Популярные авторы

Сообщества