feat(bot): add session management and user tracking

- Implement Prisma storage adapter for Telegram session management
- Add middleware to upsert Telegram user data
- Extend TelegramUser model with additional fields
- Add db:push command to turbo.json and package.json
- Extend Prisma client with exists helper method
This commit is contained in:
Yuu Ottosoka
2025-07-09 20:36:07 +03:00
parent 84760349c0
commit 49f0385d00
8 changed files with 115 additions and 15 deletions

View File

@@ -1,12 +1,22 @@
import { serve } from '@hono/node-server'; import { serve } from '@hono/node-server';
import { config } from './config'; import { config } from './config';
import { Bot, webhookCallback } from 'grammy'; import { Bot, session, webhookCallback } from 'grammy';
import { Hono } from 'hono'; import { Hono } from 'hono';
import { logger } from '@repo/logger'; import { logger } from '@repo/logger';
import { PrismaStorageAdapter } from './session-storage';
import { BotContext } from './types';
import { upsertUser } from './middlewares/upsert-user';
const bot = new Bot(config.BOT_TOKEN); const bot = new Bot<BotContext>(config.BOT_TOKEN);
const storage = new PrismaStorageAdapter();
bot.command('start', (ctx) => ctx.reply('Привет, мир')); bot.use(session({ storage, initial: () => ({}) }));
bot.use(upsertUser);
bot.command('start', async (ctx) => {
const session = await ctx.session;
await ctx.reply('Привет, ' + JSON.stringify(session));
});
if (config.MODE === 'webhook') { if (config.MODE === 'webhook') {
logger.info('Starting in webhook mode...'); logger.info('Starting in webhook mode...');

View File

@@ -0,0 +1,30 @@
import { MiddlewareFn } from 'grammy';
import { BotContext } from '../types';
import { Prisma, prisma } from '@repo/db';
export const upsertUser: MiddlewareFn<BotContext> = async (ctx, next) => {
const telegramId = ctx.from?.id.toString();
if (!telegramId) {
return next();
}
const payload: Prisma.TelegramUserUncheckedCreateInput = {
id: telegramId,
username: ctx.from?.username,
firstName: ctx.from?.first_name,
lastName: ctx.from?.last_name,
languageCode: ctx.from?.language_code,
isPremium: ctx.from?.is_premium,
};
await prisma.telegramUser.upsert({
where: {
id: telegramId,
},
create: payload,
update: payload,
});
return next();
};

View File

@@ -0,0 +1,37 @@
import { prisma } from '@repo/db';
import { StorageAdapter } from 'grammy';
export class PrismaStorageAdapter<T extends object>
implements StorageAdapter<T>
{
async read(key: string): Promise<T | undefined> {
return prisma.telegramSession
.findUnique({
where: {
id: key,
},
})
.then((session) => (session?.data as T) || undefined);
}
async write(key: string, data: T): Promise<void> {
console.log('write', data);
await prisma.telegramSession.upsert({
where: {
id: key,
},
create: {
id: key,
data,
},
update: {
data,
},
});
}
async delete(key: string): Promise<void> {
await prisma.telegramSession.delete({ where: { id: key } });
}
async has(key: string): Promise<boolean> {
return prisma.telegramSession.exists({ id: key });
}
}

5
apps/bot/src/types.ts Normal file
View File

@@ -0,0 +1,5 @@
import { Context, LazySessionFlavor } from 'grammy';
// config your session data
export type SessionData = { };
export type BotContext = Context & LazySessionFlavor<SessionData>;

View File

@@ -4,7 +4,8 @@
"scripts": { "scripts": {
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:migrate": "prisma migrate dev --skip-generate", "db:migrate": "prisma migrate dev --skip-generate",
"db:deploy": "prisma migrate deploy" "db:deploy": "prisma migrate deploy",
"db:push": "prisma db push"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.11.1" "@prisma/client": "^6.11.1"

View File

@@ -22,9 +22,13 @@ model TelegramSession {
} }
model TelegramUser { model TelegramUser {
id String @id id String @id
username String? username String?
isBlocked Boolean @default(false) isBlocked Boolean @default(false)
createdAt DateTime @default(now()) firstName String?
updatedAt DateTime @updatedAt lastName String?
languageCode String?
isPremium Boolean?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} }

View File

@@ -1,8 +1,18 @@
import { PrismaClient } from "../generated/prisma"; import { Prisma, PrismaClient as PrismaClientBase } from '../generated/prisma';
const globalForPrisma = global as unknown as { prisma: PrismaClient }; export const prisma = new PrismaClientBase().$extends({
model: {
$allModels: {
async exists<T>(
this: T,
where?: Prisma.Args<T, 'findFirst'>['where']
): Promise<boolean> {
const context = Prisma.getExtensionContext(this);
const result = await (context as any).findFirst({ where });
return result !== null;
},
},
},
});
export const prisma = export type PrismaClient = typeof prisma;
globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

View File

@@ -23,6 +23,9 @@
}, },
"db:deploy": { "db:deploy": {
"cache": false "cache": false
},
"db:push": {
"cache": false
} }
} }
} }