Telegram-бот на Node.js та grammY: покрокові діалоги, вебхуки та захист від 429

Фреймворк grammY став одним із найпопулярніших інструментів для розробки Telegram-бот ів на Node.js завдяки чудовій архітектурі, підтримці плагінів та високій продуктивності. Однак написання базового «Hello World» сильно відрізняється від створення відмовостійкого сервісу, готового до реальних навантажень і обробки бізнес-даних.

У цій статті ми розберемо побудову архітектури бота для збору лідів. Ми розглянемо безпечну ініціалізацію через переменные окружения, організацію покрокових діалогів (conversations), перемикання між Long Polling та Webhook на базі Express, а також правильну обробку лімітів Telegram Bot API (помилка 429).

1. Ініціалізація проєкту та безпечна конфігурація

Ніколи не зберігайте токен бота у вихідному коді. Для роботи з конфігурацією в Node.js використовується пакет dotenv, а сам токен виноситься у файл .env. Переконайтеся, що файл .env додано до .gitignore.

Для створення повноцінного бота нам знадобляться пакети grammy, @grammyjs/conversations (для управління сценаріями діалогу) та @grammyjs/auto-retry (для обробки лімітів API).

require('dotenv').config();
const { Bot, session } = require('grammy');

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

const bot = new Bot(botToken);

2. Організація покрокового діалогу для збору лідів

Типова задача комерційного бота — зібрати контактні дані користувача (ім'я, телефон, суть звернення) та передати їх до CRM. У grammY для цього застосовується офіційно підтримуваний плагін conversations. Він дозволяє писати асинхронний код діалогу так, ніби це лінійний скрипт, не перетворюючи логіку на ланцюжок розрізнених callback-функцій.

При цьомуважливо передбачити валідацію вхідних даних та можливість скасування сценарію на будь-якому кроці за допомогою глобальних команд (наприклад, /cancel).

const { conversations, createConversation } = require('@grammyjs/conversations');

// Подключаем сессии (необходимы для работы conversations)
bot.use(session({ initial: () => ({}) }));

/**
* Функция-сценарий пошагового сбора лида
*/
async function collectLeadConversation(conversation, ctx) {
await ctx.reply('Давайте оформим заявку! Как к вам обращаться?');

// Ожидаем текстовый ответ от пользователя
const nameContext = await conversation.waitFor('message:text');
const userName = nameContext.message.text.trim();

if (userName.startsWith('/')) {
await ctx.reply('Заполнение заявки отменено.');
return;
}

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

// Ожидаем ответ с контактом или текстом
const phoneContext = await conversation.waitFor(['message:contact', 'message:text']);
let phoneNumber = '';

if (phoneContext.message.contact) {
phoneNumber = phoneContext.message.contact.phone_number;
} else {
phoneNumber = phoneContext.message.text.trim();
}

// Генерация уникального идентификатора лида
const leadId = Math.random().toString(36).substring(2, 9);

console.log('Новая заявка сохранена:', {
leadId,
telegramId: ctx.from.id,
name: userName,
phone: phoneNumber,
createdAt: new Date().toISOString()
});

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

// Регистрируем диалог в плагине
bot.use(createConversation(collectLeadConversation));
bot.use(conversations());

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

bot.command('start', async (ctx) => {
await ctx.reply('Привет! Нажмите /lead, чтобы оставить заявку на консультацию.');
});

3. Webhook vs Long Polling: налаштування продакшн-сервера

На етапі розробки зручніше використовувати Long Polling (bot.start()) — він не вимагає публічної IP-адреси та SSL-сертифіката. Однак під високим навантаженням у продакшні стандартним рішенням є Webhook. Обработка вебхуков дозволяє розгортати бота в Serverless-інфраструктурі або масштабувати бекенд за балансувальником.

Для захисту вебхука необхідно використовувати заголовки X-Telegram-Bot-Api-Secret-Token, що передаються під час реєстрації URL через метод setWebhook.

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

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

const PORT = process.env.PORT || 3000;
const SECRET_TOKEN = process.env.WEBHOOK_SECRET || 'crypto-safe-secret-token';

if (process.env.NODE_ENV === 'production') {
// Продакшн: используем Express и Webhook с проверкой Secret Token
app.post(`/webhook/${botToken}`, (req, res, next) => {
const incomingToken = req.headers['x-telegram-bot-api-secret-token'];
if (incomingToken !== SECRET_TOKEN) {
return res.status(403).send('Forbidden: invalid secret token');
}
return next();
}, webhookCallback(bot, 'express'));

app.listen(PORT, () => {
console.log(`Webhook сервер запущен на порту ${PORT}`);
});
} else {
// Разработка: запуск Long Polling
bot.start({
onStart: (botInfo) => console.log(`Бот @${botInfo.username} запущен локально через Long Polling`),
});
}

4. Боротьба з лімітами Telegram API (Помилка 429 / Retry-After)

Telegram Bot API накладає жорсткі обмеження: не більше 30 повідомлень на секунду сумарно і не більше 1 повідомлення на секунду в один і той самий чат. При масових розсилках або різкому напливі користувачів API повертає помилку з HTTP-кодом 429 Too Many Requests і параметром retry_after.

Якщо ігнорувати цей статус і продовжувати відправку, Telegram може тимчасово або назавжди заблокувати токен бота. В екосистемі grammY для автоматичного врегулювання цієї проблеми використовується плагін @grammyjs/auto-retry. Він перехоплює помилки 429 і автоматично відкладає повторний запит на кількість секунд, вказану сервером Telegram.

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

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

// Пример безопасной отправки уведомления
async function sendNotification(chatId, text) {
try {
await bot.api.sendMessage(chatId, text, { parse_mode: 'HTML' });
} catch (error) {
console.error(`Не удалось доставить сообщение для ${chatId}:`, error.message);
}
}

5. Архітектурні нюанси та чек-лист для запуску

  • Идемпотентность вебхуков: Telegram гарантує доставку оновлень, але при збоях мережі може надіслати один і той самий update_id повторно. Зберігайте оброблювані update_id у Redis або в БД з TTL для виключення дублів.
  • Коректне завершення (Graceful Shutdown): При перезапуску застосунку зупиняйте Long Polling або закривайте з'єднання з базою даних, щоб уникати втрати поточних станів сесій.
  • Валідація типів даних: При отриманні повідомлень у сценаріях завжди перевіряйте їхній тип. Користувач може надіслати стікер, відео або файл замість очікуваного тексту.

Якщо вам потрібна професійна розробка складних ботів або интеграция Telegram-рішень з вашою корпоративною системою, зверніться до фахівців BotCreator.

Нові статті — у Telegram

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