EvolCode
треки/Телеграм-боты·01 / 06
13 мин чтения

Состояния и хранение пользователей

У эхо-бота нет памяти: каждое сообщение для него — первое. Реальный бот должен помнить пользователей (кто они, на каком тарифе, какой прогресс), и уметь вести диалог из нескольких шагов («давай зарегистрируемся: сначала имя… теперь email… теперь возраст…»). Решения два: БД для долговременной памяти и FSM (Finite State Machine) для текущего состояния разговора.

БД — где хранить пользователей

Самый простой стек: SQLite + Prisma для прототипа, Postgres + Prisma для прода. Минимальная схема: модель User с полями id (Telegram ID — это число), username, firstName, plan, createdAt, lastSeenAt. На каждый входящий update делаешь upsert: если пользователь с таким telegram_id есть — обновляешь lastSeenAt, нет — создаёшь. Это даёт тебе автоматический трекинг активной аудитории и базу для дальнейших фич (рассылки, лимиты, статистика).

Middleware — место для общей логики

В grammY есть middleware-цепочка как в Express. На каждый update проходит череда функций: сначала загружаем юзера из БД (или создаём), кладём в ctx, потом проверяем не в бане ли он, потом считаем лимиты, потом вызываем твой обработчик. Middleware = DRY. Без него ты в каждом обработчике повторяешь одно и то же. Единственное правило: не забудь вызвать await next() чтобы передать управление дальше.

FSM — конечный автомат для многошаговых форм

Тебе нужно собрать у пользователя 3 поля. Просто слать reply на каждое сообщение и проверять «это имя или email?» — кошмар. Правильно: вводишь понятие «состояние диалога»: idle, awaiting_name, awaiting_email, awaiting_age. В БД у пользователя поле fsm_state. Текущий обработчик читает state, обрабатывает соответственно, и переключает state. У grammy есть готовая @grammyjs/conversations — но для понимания паттерна сначала собери ручками.

Сессии vs БД

У grammY есть встроенный middleware session — он хранит произвольный state по chatId. Дёшево, удобно. По умолчанию хранит в памяти процесса (значит при перезапуске всё пропадёт). Можно подключить storage в Redis или БД. Когда что использовать: сессия для краткосрочного флоу (FSM формы регистрации, многошаговый ввод). БД для долгосрочных данных (профиль, тариф, прогресс). На практике — оба слоя одновременно.

Что хранить и как часто

Соблазн писать в БД на каждое сообщение огромный. Но если бот популярный — это убивает БД. Стратегия: lastSeenAt пиши не на каждое сообщение, а раз в N минут (если уже писал недавно — пропусти). Сложные операции (генерация AI-ответа, оплата) логируй в отдельную таблицу messages_log. Чтения должны быть кэшированы: профиль пользователя кэшируется в памяти на минуту-две, не достаём БД на каждый chat. Помни — если бот растёт, БД станет узким местом раньше всего остального.

примерtypescript
// schema.prisma:
// model User {
//   id              BigInt   @id  // Telegram user ID
//   username        String?
//   firstName       String?
//   plan            String   @default("free")
//   fsmState        String   @default("idle")
//   regName         String?
//   regEmail        String?
//   createdAt       DateTime @default(now())
//   lastSeenAt      DateTime @updatedAt
// }

import { Bot, Context, session, SessionFlavor } from 'grammy';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

interface SessionData {
  step: 'idle' | 'awaiting_name' | 'awaiting_email';
  draft: { name?: string; email?: string };
}
type MyContext = Context & SessionFlavor<SessionData>;

const bot = new Bot<MyContext>(process.env.BOT_TOKEN!);

bot.use(
  session({
    initial: (): SessionData => ({ step: 'idle', draft: {} }),
  })
);

// Middleware — upsert пользователя в БД на каждый update
bot.use(async (ctx, next) => {
  if (!ctx.from) return next();
  await prisma.user.upsert({
    where: { id: BigInt(ctx.from.id) },
    create: {
      id: BigInt(ctx.from.id),
      username: ctx.from.username,
      firstName: ctx.from.first_name,
    },
    update: {
      username: ctx.from.username,
      firstName: ctx.from.first_name,
      // lastSeenAt обновится автоматом через @updatedAt
    },
  });
  return next();
});

// Многошаговая форма регистрации
bot.command('register', async (ctx) => {
  ctx.session.step = 'awaiting_name';
  ctx.session.draft = {};
  await ctx.reply('Как тебя зовут?');
});

bot.on('message:text', async (ctx) => {
  const session = ctx.session;
  const text = ctx.message.text;

  if (session.step === 'awaiting_name') {
    session.draft.name = text;
    session.step = 'awaiting_email';
    return ctx.reply('Отлично. Теперь твой email:');
  }

  if (session.step === 'awaiting_email') {
    if (!/^\S+@\S+\.\S+$/.test(text)) {
      return ctx.reply('Это не похоже на email. Попробуй ещё раз:');
    }
    session.draft.email = text;
    // Сохраняем в БД
    await prisma.user.update({
      where: { id: BigInt(ctx.from!.id) },
      data: { regName: session.draft.name, regEmail: session.draft.email },
    });
    session.step = 'idle';
    session.draft = {};
    return ctx.reply(`Готово! Имя: ${session.draft.name}, email: ${session.draft.email}`);
  }

  return ctx.reply('Не понял. Напиши /register чтобы зарегистрироваться.');
});

bot.start();

Бот с БД через Prisma и многошаговой регистрацией через сессии

главное
  • 01Каждый Telegram user_id — стабильный целочисленный идентификатор. Используй его как primary key в твоей БД.
  • 02Middleware — место для общей логики (upsert, проверка бана, лимиты). Не забывай вызывать next().
  • 03FSM с полем step в сессии или БД — стандартный паттерн для многошаговых форм.
  • 04Сессии grammY для краткосрочного флоу, БД для долгосрочных данных. Они дополняют друг друга.
  • 05lastSeenAt на каждое сообщение убивает БД при росте. Дебаунс — раз в несколько минут.
проверь себя
01Что использовать как primary key в таблице users?
02Что произойдёт, если в middleware забыть await next()?
03Где хранить промежуточные шаги формы регистрации?
следующий урок: Принимаем платежи прямо в боте