Telegram Bot: webhook орнотуу, FSM паттерни жана продвинутый middleware
Telegram Bot APIнин long polling менен баштоо жеңил. Бирок чыныгы production бот башкача иштейт: webhook, FSM (Finite State Machine) аркылуу көп кадамдуу диалог, middleware chain. Бул макалада - баары реалдуу код менен.
Long Polling vs Webhook: production'до эмне тандоо керек
Long polling (bot.launch() Telegraf'та) - бот Telegram серверлерине GET суроо жиберет, жооп күтөт. Жөнөкөй, бирок:
- Сервер рестарт болгондо бир нече секунд жооп берилбейт
- Масштабдоо кыйын (бир инстанция гана)
- Трафик туура эмес бирдирилишинен эффективдүүлүк азыраак
Webhook - Telegram хабарды сиздин HTTPS эндпоинтка POST аркылуу жибергет. Жылдамыраак, масштабдалат, production стандарты.
Nginx + Node.js webhook орнотуу
# /etc/nginx/sites-available/mybot
server {
listen 443 ssl;
server_name bot.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/bot.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.yourdomain.com/privkey.pem;
location /bot-webhook {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
// src/index.ts
import { Telegraf } from 'telegraf';
import express from 'express';
const bot = new Telegraf(process.env.BOT_TOKEN!);
const app = express();
app.use(express.json());
// Webhook route
app.use(bot.webhookCallback('/bot-webhook'));
// Webhook орнотуу - бир жолу иштетилет
async function setup() {
await bot.telegram.setWebhook(
`https://bot.yourdomain.com/bot-webhook`
);
console.log('Webhook set');
}
app.listen(3000, () => setup());
Telegram'дан HTTPS талап кылынат. Let's Encrypt бекер SSL берет.
FSM: көп кадамдуу диалог архитектурасы
FSM (Finite State Machine) - пайдалануучунун абалын сактоо жана ага жараша жооп берүү паттерни. Мисал: регистрация формасы - 3 кадам.
// src/session.ts
import { session } from 'telegraf';
export interface BotSession {
step?: 'WAITING_NAME' | 'WAITING_PHONE' | 'WAITING_EMAIL' | null;
registrationData?: {
name?: string;
phone?: string;
};
}
// Жөнөкөй in-memory session (production'до Redis'ке которуу керек)
export const sessionMiddleware = session<BotSession>({
defaultSession: () => ({ step: null }),
});
// src/handlers/registration.ts
import { Telegraf, Context } from 'telegraf';
import type { BotSession } from '../session';
type BotContext = Context & { session: BotSession };
export function setupRegistrationFlow(bot: Telegraf<BotContext>) {
// /start - агымды баштоо
bot.command('start', async (ctx) => {
ctx.session.step = 'WAITING_NAME';
ctx.session.registrationData = {};
await ctx.reply(
'Каттоодон өтүүгө кош келиңиз!\n\nАтыңызды жазыңыз:'
);
});
// Бардык текст хабарларды кармоо
bot.on('text', async (ctx) => {
const text = ctx.message.text;
const step = ctx.session.step;
if (step === 'WAITING_NAME') {
if (text.length < 2) {
return ctx.reply('Ат өтө кыска. Кайра жазыңыз:');
}
ctx.session.registrationData!.name = text;
ctx.session.step = 'WAITING_PHONE';
return ctx.reply('Жакшы! Эми телефон номериңизди жазыңыз (+996xxxxxxxxx):');
}
if (step === 'WAITING_PHONE') {
const phoneRegex = /^\+996\d{9}$/;
if (!phoneRegex.test(text)) {
return ctx.reply('Формат туура эмес. Мисал: +996700123456');
}
ctx.session.registrationData!.phone = text;
ctx.session.step = 'WAITING_EMAIL';
return ctx.reply('Эми email жазыңыз:');
}
if (step === 'WAITING_EMAIL') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(text)) {
return ctx.reply('Email формат туура эмес. Кайра жазыңыз:');
}
// Каттоону аяктоо
const data = ctx.session.registrationData!;
ctx.session.step = null;
ctx.session.registrationData = {};
// Маалыматтарды сактоо...
await saveUser({ ...data, email: text, telegramId: ctx.from.id });
return ctx.reply(
`✅ Каттоо ийгиликтүү аяктады!\n\nАт: ${data.name}\nТелефон: ${data.phone}\nEmail: ${text}`
);
}
});
}
Middleware Pipeline
Telegraf'та middleware Express.js'ке окшош иштейт - функциялар чынжыры:
// src/middleware/logger.ts
export async function loggerMiddleware(ctx: BotContext, next: () => Promise<void>) {
const start = Date.now();
const userId = ctx.from?.id;
const updateType = ctx.updateType;
console.log(`[${new Date().toISOString()}] ${userId} → ${updateType}`);
await next(); // Кийинки middleware'ге өтүү
const ms = Date.now() - start;
console.log(`[${updateType}] completed in ${ms}ms`);
}
// src/middleware/throttle.ts - спам коргоо
const userLastRequest = new Map<number, number>();
export async function throttleMiddleware(ctx: BotContext, next: () => Promise<void>) {
const userId = ctx.from?.id;
if (!userId) return next();
const lastRequest = userLastRequest.get(userId) || 0;
const now = Date.now();
if (now - lastRequest < 1000) { // 1 секундга чейин 1 суроо
return ctx.reply('Сураныч, бир аз күтүңүз...');
}
userLastRequest.set(userId, now);
return next();
}
// src/middleware/auth.ts - авторизация
export async function authMiddleware(ctx: BotContext, next: () => Promise<void>) {
const userId = ctx.from?.id;
if (!userId) return;
const user = await db.user.findUnique({ where: { telegramId: userId } });
if (!user) {
return ctx.reply('Сиз катталган эмессиз. /start жазыңыз');
}
// ctx'ке пайдалануучу маалыматты кошуу
(ctx as any).dbUser = user;
return next();
}
// src/index.ts - middleware'лерди тиркөө
bot.use(sessionMiddleware);
bot.use(loggerMiddleware);
bot.use(throttleMiddleware);
// Авторизация талап кылынган бөлүктөр үчүн:
bot.command('profile', authMiddleware, async (ctx) => {
const user = (ctx as any).dbUser;
await ctx.reply(`Профиль: ${user.name}`);
});
Callback Data: 64 байтка кантип сыйгыруу
Inline keyboard callback_data 64 байтка чектелген. Маалыматтарды сыйгыруу үчүн структуралуу формат колдонуңуз:
// Структура: "action:id" же "action:id:extra"
const ACTIONS = {
ORDER_CONFIRM: 'oc',
ORDER_CANCEL: 'ox',
PAGE_NEXT: 'pn',
PAGE_PREV: 'pp',
} as const;
function makeCallback(action: keyof typeof ACTIONS, ...params: (string | number)[]) {
return [ACTIONS[action], ...params].join(':');
}
function parseCallback(data: string) {
const [actionCode, ...params] = data.split(':');
const action = Object.entries(ACTIONS).find(([, v]) => v === actionCode)?.[0];
return { action, params };
}
// Колдонуу:
await ctx.reply('Буйрутманы ырастайсызбы?', {
reply_markup: {
inline_keyboard: [[
{ text: '✅ Ырастоо', callback_data: makeCallback('ORDER_CONFIRM', orderId) },
{ text: '❌ Жокко чыгаруу', callback_data: makeCallback('ORDER_CANCEL', orderId) },
]],
},
});
bot.on('callback_query', async (ctx) => {
const data = (ctx.callbackQuery as any).data as string;
const { action, params } = parseCallback(data);
if (action === 'ORDER_CONFIRM') {
const orderId = params[0];
await confirmOrder(orderId);
await ctx.answerCbQuery('Буйрутма ырасталды!');
await ctx.editMessageText('✅ Буйрутмаңыз ырасталды');
}
});
Redis аркылуу production session
In-memory session сервер рестарт болгондо жоголот. Redis колдонуу:
import { Redis } from 'ioredis';
import { session } from 'telegraf';
const redis = new Redis(process.env.REDIS_URL!);
const redisSession = session<BotSession>({
store: {
async get(key: string) {
const data = await redis.get(`session:${key}`);
return data ? JSON.parse(data) : undefined;
},
async set(key: string, value: BotSession) {
await redis.setex(`session:${key}`, 86400, JSON.stringify(value)); // 24 саат
},
async delete(key: string) {
await redis.del(`session:${key}`);
},
},
defaultSession: () => ({ step: null }),
});
bot.use(redisSession);
Aunimeda Telegram ботторун жана Mini Appтарды иштеп чыгат.
Ошондой эле: Telegram бот бизнес үчүн, Telegram Stars монетизация