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:
@@ -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...');
|
||||||
|
|||||||
30
apps/bot/src/middlewares/upsert-user.ts
Normal file
30
apps/bot/src/middlewares/upsert-user.ts
Normal 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();
|
||||||
|
};
|
||||||
37
apps/bot/src/session-storage.ts
Normal file
37
apps/bot/src/session-storage.ts
Normal 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
5
apps/bot/src/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Context, LazySessionFlavor } from 'grammy';
|
||||||
|
|
||||||
|
// config your session data
|
||||||
|
export type SessionData = { };
|
||||||
|
export type BotContext = Context & LazySessionFlavor<SessionData>;
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
},
|
},
|
||||||
"db:deploy": {
|
"db:deploy": {
|
||||||
"cache": false
|
"cache": false
|
||||||
|
},
|
||||||
|
"db:push": {
|
||||||
|
"cache": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user