Як написати 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

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.