Как написать Telegram-бота для сбора лидов на Node.js и grammY: пошаговый диалог, Webhook и защита от 429

Многие разработчики до сих пор начинающие проекты на Node.js выбирают библиотеку node-telegram-bot-api, несмотря на то что её архитектура морально устарела, а поддержка современных функций Telegram Bot API оставляет желать лучшего. Сегодня индустриальным стандартом для JavaScript и TypeScript является фреймворк grammY. Он спроектирован на основе Middleware-цепочек (аналогично Express и Koa), поддерживает плагины сессий и пошаговых сценариев, а также предоставляет встроенную защиту от превышения лимитов API.

В этом руководстве мы соберем готового к продакшену бота для приема заявок от клиентов. Мы откажемся от простых примитивов «команда — ответ» и реализуем полноценный пошаговый диалог с сохранением данных в базу, настроим защищенный Webhook и обойдем лимит 429 Too Many Requests.

Почему grammY, а не node-telegram-bot-api или Telegraf

Главное преимущество grammY — архитектура обработки входящих обновлений (Updates). Каждый Update проходит через цепочку intermediate-функций. Это позволяет гибко изолировать аутентификацию, сессии и обработку команд. Кроме того, grammY из коробки решает проблему с отбросом застрявших запросов и предлагает официальные модули для работы с диалогами (@grammyjs/conversations) и автоматическими повторными запросами при ошибочных HTTP-ответах (@grammyjs/auto-retry).

Безопасный старт и организация кода

Никогда не храните токен бота в теле файла. Токен дает полный доступ к управлению вашим ботом и его отправке сообщений от имени компании. Используйте переменные окружения (process.env.BOT_TOKEN) и проверяйте их наличие при старте приложения.

Для сборки сценария заявки нам понадобятся следующие пакеты:

  • grammy — ядро фреймворка;
  • @grammyjs/conversations — плагин для реализации пошаговых диалогов (State Machine);
  • express — HTTP-сервер для принятия Webhook.

Реализация пошагового сбора лидов (Conversations)

Вместо того чтобы вручную хранить текущий шаг пользователя (например, step = 'awaiting_phone') в глобальном объекте или базе данных, модуль conversations позволяет писать асинхронный линейный код. Функция «засыпает» на моменте ожидания ответа от пользователя и просыпается при получении следующего сообщения.

Ниже представлен рабочий пример инициализации бота и создания сценария записи лида. При успешном завершении диалога мы генерируем уникальный идентификатор заявки через модуль crypto и отправляем данные во внешнее хранилище.

const { Bot, session } = require('grammy');
const { conversations, createConversation } = require('@grammyjs/conversations');
const crypto = require('crypto');

const botToken = process.env.BOT_TOKEN;
if (!botToken) {
throw new Error('Критическая ошибка: переменная окружения BOT_TOKEN не задана!');
}

const bot = new Bot(botToken);

// Инициализация хранилища сессий для плагина conversations
bot.use(session({ initial: () => ({}) }));
bot.use(conversations());

/**
* Пошаговый сценарий сбора данных лида
*/
async function collectLeadConversation(conversation, ctx) {
await ctx.reply('Здравствуйте! Как к вам обращаться? Напишите ваше имя:');

const nameCtx = await conversation.waitFor('message:text');
const userName = nameCtx.message.text.trim();

await ctx.reply(`Приятно познакомиться, ${userName}! Укажите ваш контактный номер телефона:`);

const phoneCtx = await conversation.waitFor('message:text');
const userPhone = phoneCtx.message.text.trim();

// Генерация уникального идентификатора заявки
const leadId = crypto.randomBytes(7).toString('hex');
const telegramId = ctx.from.id;

// Безопасный вызов внешних асинхронных функций внутри conversation
await conversation.external(async () => {
// В реальном проекте здесь вызывается INSERT в PostgreSQL / MySQL / MongoDB
console.log(`[БД] Сохранение лида #${leadId}:`, {
id: leadId,
telegramId,
name: userName,
phone: userPhone,
createdAt: new Date().toISOString()
});
});

await ctx.reply(`Спасибо! Ваша заявка #${leadId} принята. Менеджер свяжется с вами в ближайшее время.`);
}

// Регистрация диалога в боте
bot.use(createConversation(collectLeadConversation));

// Команда запуска бота
bot.command('start', async (ctx) => {
await ctx.reply('Вас приветствует отдел продаж! Для подачи заявки нажмите /lead');
});

// Запуск сценария сбора лида
bot.command('lead', async (ctx) => {
await ctx.conversation.enter('collectLeadConversation');
});

Защита от ошибки 429 Too Many Requests

Telegram Bot API накладывает строгие лимиты на частоту отправки сообщений:

  • Не более 30 сообщений в секунду суммарно по всем пользователям;
  • Не более 1 сообщения в секунду в один и тот же приватный чат;
  • Не более 20 сообщений в минуту в одну группу или канал.

Если вы отправляете массовую рассылку или получаете всплеск трафика, Telegram вернет ошибку с HTTP-кодом 429 и заголовком Retry-After. Простая попытка проигнорировать эту ошибку приведет к тому, что сообщения пользователей будут потеряны.

В grammY данная проблема решается подключением API Transformer @grammyjs/auto-retry. Этот плагин перехватывает ошибки 429, временно приостанавливает отправку для конкретного чата и повторно делает запрос через промежуток времени, указанный сервером Telegram.

const { autoRetry } = require('@grammyjs/auto-retry');

// Подключение авто-ретрая к API-клиенту бота
bot.api.config.use(
autoRetry({
maxRetryAttempts: 3, // Максимальное количество повторных попыток
maxDelaySeconds: 60, // Максимальное время ожидания (в секундах)
})
);

// Глобальный обработчик ошибок (Catch-all)
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Ошибка при обработке update_id ${ctx.update.update_id}:`);
const e = err.error;
if (e instanceof Error) {
console.error('Сообщение об ошибке:', e.message);
}
});

Настройка Webhook для продакшена vs Long Polling для разработки

При локальной разработке удобнее использовать метод bot.start() (Long Polling). Однако в боевом окружении под высокой нагрузкой необходим Webhook.

При использовании Webhook критически важно обеспечивать безопасность: Telegram передает вам обновления по публичному URL. Чтобы злоумышленники не могли подделать отправку Updates, отправляя POST-запросы на ваш сервер, используйте заголовок secret_token. Telegram прикрепляет этот токен к каждому HTTP-запросу в заголовке X-Telegram-Bot-Api-Secret-Token.

const express = require('express');
const { webhookCallback } = require('grammy');

const app = express();
app.use(express.json());

const SECRET_TOKEN = process.env.WEBHOOK_SECRET || 'a_very_secret_hex_string_64_chars';
const PORT = process.env.PORT || 3000;
const WEBHOOK_URL = process.env.WEBHOOK_URL; // Например: https://api.yourdomain.com/telegram/webhook

// Middleware для проверки секретного токена от Telegram
app.use('/telegram/webhook', (req, res, next) => {
const incomingToken = req.headers['x-telegram-bot-api-secret-token'];
if (incomingToken !== SECRET_TOKEN) {
console.warn('[Security Warning] Отклонен запрос с невалидным secret_token');
return res.status(403).json({ error: 'Access denied: invalid secret token' });
}
next();
});

// Передача обработки входящих данных в grammY
app.post('/telegram/webhook', webhookCallback(bot, 'express'));

if (process.env.NODE_ENV === 'production') {
app.listen(PORT, async () => {
console.log(`Webhook сервер успешно запущен на порту ${PORT}`);

// Установка вебхука в Telegram API при старте сервера
await bot.api.setWebhook(WEBHOOK_URL, {
secret_token: SECRET_TOKEN,
drop_pending_updates: true // Пропускаем накопившиеся сообщения при перезапуске
});
console.log(`Webhook установлена на URL: ${WEBHOOK_URL}`);
});
} else {
// Режим разработки: запускаем Long Polling
bot.start({
onStart: (botInfo) => {
console.log(`Бот @${botInfo.username} запущен локально в режиме Long Polling`);
},
});
}

Чеклист готовности бота к продакшену

  1. Все секреты в ENV: Никаких токенов API и ключей БД в открытом коде.
  2. Идемпотентность и дубликаты: Webhook может присылать один и тот же update_id повторно при сетевых сбоях. Сохраняйте обработанные update_id в Redis или Postgres и завершайте ответ со статусом 200 OK в течение 1–2 секунд.
  3. Логирование ошибок: Обязательно настраивайте bot.catch(), чтобы падение одной сессии не останавливало весь Node.js процесс.
  4. Контроль лимитов: Всегда подключайте @grammyjs/auto-retry для защиты от банов по ошибке 429.

Если вам необходима квалифицированная помощь в проектировании архитектуры Telegram-ботов, создании Mini Apps или интеграции CRM-систем, обратитесь к специалистам сервиса BotCreator.

Новые статьи — в Telegram

Разбираем, что автоматизировать в бизнесе и как это работает на практике. Без спама.