From 49f0385d007e8a8a244d8b8e7d61ef7ac0883f6d Mon Sep 17 00:00:00 2001 From: Yuu Ottosoka Date: Wed, 9 Jul 2025 20:36:07 +0300 Subject: [PATCH] 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 --- apps/bot/src/index.ts | 16 +++++++++-- apps/bot/src/middlewares/upsert-user.ts | 30 ++++++++++++++++++++ apps/bot/src/session-storage.ts | 37 +++++++++++++++++++++++++ apps/bot/src/types.ts | 5 ++++ packages/database/package.json | 3 +- packages/database/prisma/schema.prisma | 14 ++++++---- packages/database/src/client.ts | 22 +++++++++++---- turbo.json | 3 ++ 8 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 apps/bot/src/middlewares/upsert-user.ts create mode 100644 apps/bot/src/session-storage.ts create mode 100644 apps/bot/src/types.ts diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index faa892c..6b1fbac 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -1,12 +1,22 @@ import { serve } from '@hono/node-server'; import { config } from './config'; -import { Bot, webhookCallback } from 'grammy'; +import { Bot, session, webhookCallback } from 'grammy'; import { Hono } from 'hono'; 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(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') { logger.info('Starting in webhook mode...'); diff --git a/apps/bot/src/middlewares/upsert-user.ts b/apps/bot/src/middlewares/upsert-user.ts new file mode 100644 index 0000000..34d1e1e --- /dev/null +++ b/apps/bot/src/middlewares/upsert-user.ts @@ -0,0 +1,30 @@ +import { MiddlewareFn } from 'grammy'; +import { BotContext } from '../types'; +import { Prisma, prisma } from '@repo/db'; + +export const upsertUser: MiddlewareFn = 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(); +}; diff --git a/apps/bot/src/session-storage.ts b/apps/bot/src/session-storage.ts new file mode 100644 index 0000000..1421835 --- /dev/null +++ b/apps/bot/src/session-storage.ts @@ -0,0 +1,37 @@ +import { prisma } from '@repo/db'; +import { StorageAdapter } from 'grammy'; + +export class PrismaStorageAdapter + implements StorageAdapter +{ + async read(key: string): Promise { + return prisma.telegramSession + .findUnique({ + where: { + id: key, + }, + }) + .then((session) => (session?.data as T) || undefined); + } + async write(key: string, data: T): Promise { + console.log('write', data); + await prisma.telegramSession.upsert({ + where: { + id: key, + }, + create: { + id: key, + data, + }, + update: { + data, + }, + }); + } + async delete(key: string): Promise { + await prisma.telegramSession.delete({ where: { id: key } }); + } + async has(key: string): Promise { + return prisma.telegramSession.exists({ id: key }); + } +} diff --git a/apps/bot/src/types.ts b/apps/bot/src/types.ts new file mode 100644 index 0000000..de93122 --- /dev/null +++ b/apps/bot/src/types.ts @@ -0,0 +1,5 @@ +import { Context, LazySessionFlavor } from 'grammy'; + +// config your session data +export type SessionData = { }; +export type BotContext = Context & LazySessionFlavor; diff --git a/packages/database/package.json b/packages/database/package.json index 2073d79..e4e144b 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -4,7 +4,8 @@ "scripts": { "db:generate": "prisma generate", "db:migrate": "prisma migrate dev --skip-generate", - "db:deploy": "prisma migrate deploy" + "db:deploy": "prisma migrate deploy", + "db:push": "prisma db push" }, "dependencies": { "@prisma/client": "^6.11.1" diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index c750a7f..3413e72 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -22,9 +22,13 @@ model TelegramSession { } model TelegramUser { - id String @id - username String? - isBlocked Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id + username String? + isBlocked Boolean @default(false) + firstName String? + lastName String? + languageCode String? + isPremium Boolean? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/packages/database/src/client.ts b/packages/database/src/client.ts index 67d431b..837b618 100644 --- a/packages/database/src/client.ts +++ b/packages/database/src/client.ts @@ -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( + this: T, + where?: Prisma.Args['where'] + ): Promise { + const context = Prisma.getExtensionContext(this); + const result = await (context as any).findFirst({ where }); + return result !== null; + }, + }, + }, +}); -export const prisma = - globalForPrisma.prisma || new PrismaClient() - -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; +export type PrismaClient = typeof prisma; diff --git a/turbo.json b/turbo.json index ace97ab..c2f8006 100644 --- a/turbo.json +++ b/turbo.json @@ -23,6 +23,9 @@ }, "db:deploy": { "cache": false + }, + "db:push": { + "cache": false } } }