Что это решает
Любому внутреннему инструменту нужен интерфейс, а интерфейс — это фронтенд, авторизация, вёрстка под телефон и уговоры сотрудников что-то установить. Бот в Telegram снимает всё сразу: приложение у людей уже стоит, вход уже выполнен, уведомления уже настроены. Внутренний отчёт, приём заявки, кнопка согласования и алерт из мониторинга собираются на HTTP-запросах, без единой строки клиентского кода. Для внешних клиентов работает тот же расчёт: путь от ссылки до первого действия короче, чем у любого сайта с регистрацией.
Как устроено
Токен и вызовы
Бот создаётся в @BotFather, который выдаёт токен вида <числовой id>:<строка>. Токен — полный доступ к боту, его хватает для чтения переписки и отправки сообщений от имени бота; при утечке спасает только /revoke в том же BotFather. Все методы дёргаются обычным HTTPS-запросом по адресу https://api.telegram.org/bot<токен>/<метод>, параметры передаются в query-строке, JSON-теле или multipart при загрузке файлов. Ответ всегда одной формы: {"ok": true, "result": ...} либо {"ok": false, "error_code": ..., "description": ...} — описание ошибки текстовое и читаемое, его стоит логировать целиком.
Два способа получать события
Long polling: метод getUpdates держит соединение и отдаёт новые события. Подтверждение обработки — параметр offset, равный update_id последнего обработанного апдейта плюс единица. Пока подтверждения нет, те же события придут снова. Ничего публичного наружу не нужно, поэтому режим удобен для разработки и для внутренних ботов за NAT.
Вебхук: метод setWebhook регистрирует адрес, и Telegram сам шлёт туда POST с апдейтом. Требования жёсткие — HTTPS с валидным сертификатом и один из портов 443, 80, 88 или 8443. Параметр secret_token заставляет Telegram присылать заголовок X-Telegram-Bot-Api-Secret-Token, по которому приёмник отличает настоящий вызов от постороннего запроса на открытый URL. Параметр allowed_updates ограничивает типы событий и заодно экономит трафик. Одновременно два режима не работают: включённый вебхук выключает getUpdates.
Апдейт
Событие приходит объектом Update. Внутри — одно из полей: message, edited_message, channel_post, callback_query (нажатие инлайн-кнопки), inline_query, my_chat_member и chat_member (изменение прав и вход-выход участников), pre_checkout_query и successful_payment для платежей, poll_answer для опросов. Поле update_id монотонно растёт и служит ключом дедупликации: при повторной доставке номер тот же.
Отправка и правка
sendMessage с parse_mode в HTML или MarkdownV2. MarkdownV2 требует экранирования доброго десятка спецсимволов, и любая точка или дефис из данных пользователя роняет запрос с ошибкой разбора — на практике HTML предсказуемее. Медиа отправляются методами sendPhoto, sendDocument, sendMediaGroup. Отдельная сила — editMessageText и editMessageReplyMarkup: интерфейс перерисовывается на месте, вместо того чтобы засорять чат новыми сообщениями. После нажатия инлайн-кнопки обязателен answerCallbackQuery, иначе у нажавшего несколько секунд крутится индикатор ожидания.
Кнопки и вход в диалог
Инлайн-клавиатура крепится к сообщению, её кнопки несут callback_data — до 64 байт. Туда кладётся короткий ключ действия, а состояние хранится на своей стороне; попытка запихнуть в кнопку всю полезную нагрузку упирается в лимит на первом же длинном названии. Обычная reply-клавиатура заменяет пользователю ввод команд. Кнопки запроса контакта и геопозиции получают телефон и координаты только с явного согласия. Диплинк t.me/<имя_бота>?start=<payload> передаёт в первое сообщение произвольную метку — так связывается пользователь Telegram с записью во внутренней системе.
Файлы
У каждого загруженного файла есть file_id, по которому его можно переслать снова без повторной загрузки. Для скачивания вызывается getFile, он отдаёт путь, дальше файл забирается обычным GET. Ограничения Bot API: скачивание — до 20 МБ, отправка — до 50 МБ. Оба потолка снимаются собственным сервером Bot API, который разворачивается рядом со своим приложением.
Mini Apps и платежи
Mini App — веб-страница, открывающаяся внутри клиента; она получает подписанный блок initData. Подпись проверяется на сервере по токену бота, и без этой проверки любой подставит чужой идентификатор пользователя. Платежи подключаются через провайдера в BotFather: sendInvoice выставляет счёт, pre_checkout_query требует быстрого подтверждения от бота, successful_payment приходит после оплаты. Для цифровых товаров есть внутренняя валюта Stars.
Лимиты
При превышении частоты приходит ошибка 429 с полем retry_after — число секунд до следующей попытки. Ориентиры документации: порядка тридцати сообщений в секунду суммарно и около двадцати в минуту в одну группу. Рассылка на большую базу без очереди с паузами упирается в это на первой же сотне получателей.
Полезные сценарии
- Алерты мониторинга в тему рабочей группы с кнопкой «взял в работу», которая правит само сообщение.
- Приём заявок анкетой из нескольких шагов с записью результата в базу и диплинком на карточку в CRM.
- Внутренние команды вместо доступа в админку: короткий отчёт по запросу тем, кому не нужен полный кабинет.
- Согласование: инлайн-кнопки подтверждения и отказа, решение пишется в систему, текст сообщения заменяется на итог с именем и временем.
- Канал плюс бот-администратор для публикации по расписанию из общей контент-базы.
Ограничения
Бот не может написать первым. Пока человек не нажал /start или не добавил бота в группу, отправка невозможна. Массовая рассылка по номерам телефонов средствами Bot API не делается никак.
В группах по умолчанию включён privacy mode: бот видит только команды, ответы на свои сообщения и упоминания. Полный доступ к сообщениям группы даёт отключение режима в BotFather или права администратора — и это осознанное расширение прав, а не настройка по умолчанию.
Истории до себя бот не получает. Добавили в группу — виден поток с этого момента, прошлое недоступно.
Телефон и почта приходят только по кнопке, которую нажал сам пользователь. Идентификатор пользователя постоянный, имя и username меняются в любой момент — связывать записи можно только по числовому id.
Апдейты хранятся на стороне Telegram не дольше суток. Приёмник, лежавший дольше, теряет события безвозвратно, и восстановить их запросом нельзя.
При вебхуке порядок доставки не гарантирован, а повторы возможны. Обработчик обязан быть идемпотентным по update_id.
Формальные рамки: длина сообщения до 4096 символов, подпись к медиа до 1024, callback_data до 64 байт. Длинный отчёт режется на части на своей стороне.
Пользователь может заблокировать бота в один клик — тогда отправка возвращает 403 с описанием про блокировку, и такие адресаты помечаются, иначе очередь рассылки бесконечно долбится в мёртвые чаты.
Вся конструкция висит на одной платформе и одном токене. Резервного канала уведомлений это не отменяет.
Как проверить результат
getMe — первое, что вызывается: отвечает, значит токен жив и сеть до API есть.
getWebhookInfo — главный диагностический метод для молчащего бота. В ответе видно зарегистрированный адрес, число неразобранных апдейтов (pending_update_count), время и текст последней ошибки доставки. Растущая очередь и повторяющийся текст ошибки называют причину прямо: не тот сертификат, не тот порт, приёмник отвечает не 200.
Отправка проверяется вручную: curl с методом sendMessage в свой собственный чат. Прошло — проблема в логике приложения, не прошло — в токене, сети или правах в чате.
Сырой JSON апдейта стоит логировать целиком до разбора. Половина странностей объясняется тем, что пришло не то поле, которого ждали: пересланное сообщение, правка старого, событие о смене прав вместо текста.
Дедупликация проверяется повторной подачей того же апдейта на эндпоинт: результат в базе должен остаться один.
Вебхук на этапе разработки заводится через туннель до локальной машины, а не правкой боевого адреса. Переключение туда-обратно делается методами setWebhook и deleteWebhook, и после каждого переключения полезно снова посмотреть getWebhookInfo.
В логах отдельно считаются коды 429 и 403: первый означает, что нужна очередь с паузами, второй — что часть базы получателей мертва и её надо чистить.
Подпись Mini App тестируется атакой на себя: в initData подменяется идентификатор пользователя, запрос отправляется на сервер, ответ обязан быть отказом.
