Биз жөнүндөБлогБайланыш
Иштеп чыгуу2026-ж., 17-апрель 5 мин 12

Telegram Bot: webhook орнотуу, FSM паттерни жана продвинутый middleware

AunimedaAunimeda
📋 Мазмуну

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 монетизация

Ошондой эле окуңуз

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керекaunimeda
Иштеп чыгуу

WebSockets vs SSE vs Long Polling: realtime технологиясын кантип тандоо керек

Чат, кабарлар, заказ статусу - булардын баары реалдуу убакытта жаңыртууну талап кылат. WebSocket, Server-Sent Events жана Long Polling ар башка иштейт. Кайсын качан колдонуу керегин Node.js код мисалдары менен карайбыз.

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керекaunimeda
Иштеп чыгуу

Node.js vs Bun vs Deno 2026: кайсы JavaScript runtime тандоо керек

Bun 1.x продакшн стабилдүү. Deno 2.0 npm колдойт. Node.js 22 TypeScript'ти нативдүү иштетет. Реалдуу benchmark'тар, экосистема салыштыруусу жана Кыргызстандагы долбоорлор үчүн конкреттүү сунуштамалар.

Node.js'теги таза архитектура: бизнес логиканы инфраструктурадан бөлүп алууaunimeda
Иштеп чыгуу

Node.js'теги таза архитектура: бизнес логиканы инфраструктурадан бөлүп алуу

Алты айдан кийин Node.js долбоору спагетти кодго айланбасын десеңиз - Use Cases, Repository Pattern жана Dependency Inversion. Маалымат базасыз тестирленген бизнес логика - реалдуу код мисалдары менен.

Бизнесиңизге IT иштеп чыгуу керекпи?

Веб-сайттарды, мобилдик тиркемелерди жана AI чечимдерин иштеп чыгабыз. Акысыз консультация.

Консультация алуу Бардык макалалар