Состояния и хранение пользователей
У эхо-бота нет памяти: каждое сообщение для него — первое. Реальный бот должен помнить пользователей (кто они, на каком тарифе, какой прогресс), и уметь вести диалог из нескольких шагов («давай зарегистрируемся: сначала имя… теперь 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. Помни — если бот растёт, БД станет узким местом раньше всего остального.
// 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 и многошаговой регистрацией через сессии