From e4233c506bcae5166de2e7ce521fe749ec0b1eb3 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Sat, 16 Aug 2025 16:43:12 +0300 Subject: [PATCH] feat: update bot dependencies to include @grammyjs/runner and @grammyjs/storage-redis; enhance session management and rate limiting configuration --- apps/bot/locales/en.ftl | 2 +- apps/bot/locales/ru.ftl | 2 +- apps/bot/package.json | 2 ++ apps/bot/src/bot/context.ts | 7 +++++-- apps/bot/src/bot/features/download.ts | 4 ++-- apps/bot/src/bot/index.ts | 10 ++++++++-- apps/bot/src/bot/middlewares/index.ts | 1 + apps/bot/src/bot/middlewares/session.ts | 20 ++++++++++++++++++++ apps/bot/src/config/env.ts | 6 +++++- apps/bot/src/config/redis.ts | 3 ++- apps/bot/src/index.ts | 9 +++++---- apps/bot/src/utils/session.ts | 5 +++++ pnpm-lock.yaml | 22 ++++++++++++++++++++++ 13 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 apps/bot/src/bot/middlewares/session.ts create mode 100644 apps/bot/src/utils/session.ts diff --git a/apps/bot/locales/en.ftl b/apps/bot/locales/en.ftl index c1b9e18..080fd70 100644 --- a/apps/bot/locales/en.ftl +++ b/apps/bot/locales/en.ftl @@ -5,7 +5,7 @@ start = err-invalid-url = ❌ Invalid URL! Please send a valid TikTok link (e.g., https://vt.tiktok.com/...) err-invalid-download-urls = 🔍 Download links not found. The video might be deleted or unavailable err-generic = ⚠️ Something went wrong. Please try again in a few seconds -err-limit-exceeded = 🚫 Too many requests! Please wait before sending the next link +err-limit-exceeded = 🚫 Too many requests! Please wait msg-welcome = Welcome! I can download TikTok videos and images for you without watermark. Just send me the link (for example: https://vt.tiktok.com/...) \ No newline at end of file diff --git a/apps/bot/locales/ru.ftl b/apps/bot/locales/ru.ftl index 40ba25d..54b941c 100644 --- a/apps/bot/locales/ru.ftl +++ b/apps/bot/locales/ru.ftl @@ -5,7 +5,7 @@ start = err-invalid-url = ❌ Неверная ссылка! Отправьте корректную ссылку TikTok (например: https://vt.tiktok.com/...) err-invalid-download-urls = 🔍 Не удалось найти ссылки для скачивания. Возможно, видео удалено или недоступно err-generic = ⚠️ Что-то пошло не так. Попробуйте еще раз через несколько секунд -err-limit-exceeded = 🚫 Слишком много запросов! Подождите немного перед следующей ссылкой +err-limit-exceeded = 🚫 Слишком много запросов! Подождите немного msg-welcome = Добро пожаловать! Я могу скачать для вас видео и изображения из TikTok без водяного знака. Для этого просто отправьте мне ссылку (например: https://vt.tiktok.com/...) \ No newline at end of file diff --git a/apps/bot/package.json b/apps/bot/package.json index a5693b1..59aec29 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -24,6 +24,8 @@ "@grammyjs/i18n": "^1.1.2", "@grammyjs/parse-mode": "^2.2.0", "@grammyjs/ratelimiter": "^1.2.1", + "@grammyjs/runner": "^2.0.3", + "@grammyjs/storage-redis": "^2.5.1", "@grammyjs/types": "^3.21.0", "@repo/typescript-config": "workspace:*", "@tobyg74/tiktok-api-dl": "^1.3.4", diff --git a/apps/bot/src/bot/context.ts b/apps/bot/src/bot/context.ts index 0b0d24c..d3c854d 100644 --- a/apps/bot/src/bot/context.ts +++ b/apps/bot/src/bot/context.ts @@ -3,13 +3,16 @@ import { type AutoChatActionFlavor } from '@grammyjs/auto-chat-action'; import { type CommandsFlavor } from '@grammyjs/commands'; import { type HydrateFlavor } from '@grammyjs/hydrate'; import { type I18nFlavor } from '@grammyjs/i18n'; -import { type Context as DefaultContext } from 'grammy'; +import { type Context as DefaultContext, type SessionFlavor } from 'grammy'; export type Context = HydrateFlavor< AutoChatActionFlavor & CommandsFlavor & DefaultContext & - I18nFlavor & { + I18nFlavor & + SessionFlavor & { logger: typeof logger; } >; + +export type SessionData = {}; diff --git a/apps/bot/src/bot/features/download.ts b/apps/bot/src/bot/features/download.ts index 91066bd..d490b78 100644 --- a/apps/bot/src/bot/features/download.ts +++ b/apps/bot/src/bot/features/download.ts @@ -1,7 +1,7 @@ /* eslint-disable consistent-return */ import { type Context } from '../context'; import { logHandle } from '../helpers/logging'; -import { TTL } from '@/config/redis'; +import { TTL_URLS } from '@/config/redis'; import { getRedisInstance } from '@/utils/redis'; import { validateTikTokUrl } from '@/utils/urls'; import { Downloader } from '@tobyg74/tiktok-api-dl'; @@ -39,7 +39,7 @@ feature.on('message:text', logHandle('download-message'), async (context) => { if (result?.type === 'video' && videoUrl) { const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl })); - await redis.set(url, video.file_id, 'EX', TTL); + await redis.set(url, video.file_id, 'EX', TTL_URLS); return; } diff --git a/apps/bot/src/bot/index.ts b/apps/bot/src/bot/index.ts index d5ecceb..201bd6b 100644 --- a/apps/bot/src/bot/index.ts +++ b/apps/bot/src/bot/index.ts @@ -5,12 +5,15 @@ import * as features from './features'; import { errorHandler } from './handlers/errors'; import { i18n } from './i18n'; import * as middlewares from './middlewares'; +import { session } from './middlewares'; import { env } from '@/config/env'; import { logger } from '@/utils/logger'; import { getRedisInstance } from '@/utils/redis'; +import { getSessionKey } from '@/utils/session'; import { autoChatAction } from '@grammyjs/auto-chat-action'; import { hydrate } from '@grammyjs/hydrate'; import { limit } from '@grammyjs/ratelimiter'; +import { sequentialize } from '@grammyjs/runner'; import { Bot } from 'grammy'; type Parameters_ = { @@ -34,12 +37,12 @@ export function createBot({ apiRoot, token }: Parameters_) { keyGenerator: (ctx) => { return ctx.from?.id.toString(); }, - limit: 1, + limit: env.RATE_LIMIT, onLimitExceeded: async (ctx) => { await ctx.reply(ctx.t('err-limit-exceeded')); }, storageClient: redis, - timeFrame: env.RATE_LIMIT, + timeFrame: env.RATE_LIMIT_TIME, }), ); @@ -53,6 +56,9 @@ export function createBot({ apiRoot, token }: Parameters_) { const protectedBot = bot.errorBoundary(errorHandler); + protectedBot.use(sequentialize(getSessionKey)); + protectedBot.use(session()); + protectedBot.use(middlewares.updateLogger()); protectedBot.use(setCommands); protectedBot.use(autoChatAction(bot.api)); diff --git a/apps/bot/src/bot/middlewares/index.ts b/apps/bot/src/bot/middlewares/index.ts index 5b7aa7a..524b5f7 100644 --- a/apps/bot/src/bot/middlewares/index.ts +++ b/apps/bot/src/bot/middlewares/index.ts @@ -1 +1,2 @@ +export * from './session'; export * from './update-logger'; diff --git a/apps/bot/src/bot/middlewares/session.ts b/apps/bot/src/bot/middlewares/session.ts new file mode 100644 index 0000000..209e6f6 --- /dev/null +++ b/apps/bot/src/bot/middlewares/session.ts @@ -0,0 +1,20 @@ +import { type Context } from '@/bot/context'; +import { TTL_SESSION } from '@/config/redis'; +import { getRedisInstance } from '@/utils/redis'; +import { getSessionKey } from '@/utils/session'; +import { RedisAdapter } from '@grammyjs/storage-redis'; +import { session as createSession, type Middleware } from 'grammy'; + +const storage = new RedisAdapter({ + autoParseDates: true, + instance: getRedisInstance(), + ttl: TTL_SESSION, +}); + +export function session(): Middleware { + return createSession({ + getSessionKey, + initial: () => ({}), + storage, + }); +} diff --git a/apps/bot/src/config/env.ts b/apps/bot/src/config/env.ts index 7f2df13..1f6033f 100644 --- a/apps/bot/src/config/env.ts +++ b/apps/bot/src/config/env.ts @@ -6,7 +6,11 @@ export const envSchema = z.object({ RATE_LIMIT: z .string() .transform((value) => Number.parseInt(value, 10)) - .default('5000'), + .default('2'), + RATE_LIMIT_TIME: z + .string() + .transform((value) => Number.parseInt(value, 10)) + .default('3000'), REDIS_HOST: z.string().default('redis'), REDIS_PASSWORD: z.string(), REDIS_PORT: z diff --git a/apps/bot/src/config/redis.ts b/apps/bot/src/config/redis.ts index 0150fcd..0b9cfe8 100644 --- a/apps/bot/src/config/redis.ts +++ b/apps/bot/src/config/redis.ts @@ -1 +1,2 @@ -export const TTL = 12 * 60 * 60; // 12 hours in seconds +export const TTL_URLS = 12 * 60 * 60; // 12 hours in seconds +export const TTL_SESSION = 5 * 60; // 5 minutes in seconds diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index 1436462..5e03ed6 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -2,12 +2,15 @@ import { createBot } from './bot'; import { env as environment } from './config/env'; import { logger } from './utils/logger'; import { getRedisInstance } from './utils/redis'; +import { run } from '@grammyjs/runner'; const bot = createBot({ apiRoot: environment.TELEGRAM_API_ROOT, token: environment.BOT_TOKEN, }); +const runner = run(bot); + const redis = getRedisInstance(); // Graceful shutdown function @@ -16,7 +19,7 @@ async function gracefulShutdown(signal: string) { try { // Stop the bot - await bot.stop(); + await runner.stop(); logger.info('Bot stopped'); // Disconnect Redis @@ -33,6 +36,4 @@ async function gracefulShutdown(signal: string) { process.once('SIGINT', () => gracefulShutdown('SIGINT')); process.once('SIGTERM', () => gracefulShutdown('SIGTERM')); -bot.start({ - onStart: ({ username }) => logger.info(`Bot ${username} started`), -}); +logger.info('Bot started'); diff --git a/apps/bot/src/utils/session.ts b/apps/bot/src/utils/session.ts new file mode 100644 index 0000000..6ac5fb4 --- /dev/null +++ b/apps/bot/src/utils/session.ts @@ -0,0 +1,5 @@ +import { type Context } from '@/bot/context'; + +export function getSessionKey(ctx: Omit) { + return ctx.chat?.id.toString(); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0412a4..c49f0cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,12 @@ importers: '@grammyjs/ratelimiter': specifier: ^1.2.1 version: 1.2.1 + '@grammyjs/runner': + specifier: ^2.0.3 + version: 2.0.3(grammy@1.37.1) + '@grammyjs/storage-redis': + specifier: ^2.5.1 + version: 2.5.1 '@grammyjs/types': specifier: ^3.21.0 version: 3.21.0 @@ -530,6 +536,15 @@ packages: '@grammyjs/ratelimiter@1.2.1': resolution: {integrity: sha512-4bmVUBCBnIb2epbDiBLCvvnVjaYg7kDCPR1Ptt6gqoxm5vlD8BjainYv+yjF6221hu2KUv8QAckumDI+6xyGsQ==} + '@grammyjs/runner@2.0.3': + resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==} + engines: {node: '>=12.20.0 || >=14.13.1'} + peerDependencies: + grammy: ^1.13.1 + + '@grammyjs/storage-redis@2.5.1': + resolution: {integrity: sha512-Rz7bnrtDz8NOjgSRR4n/rBciHKSvmkrgIUc+8Yb96tSsecBlA91MGOmcgmQXKxb7gTmwliF37VgmMT85bsCZtA==} + '@grammyjs/types@3.21.0': resolution: {integrity: sha512-IMj0EpmglPCICuyfGRx4ENKPSuzS2xMSoPgSPzHC6FtnWKDEmJLBP/GbPv/h3TAeb27txqxm/BUld+gbJk6ccQ==} @@ -4310,6 +4325,13 @@ snapshots: '@grammyjs/ratelimiter@1.2.1': {} + '@grammyjs/runner@2.0.3(grammy@1.37.1)': + dependencies: + abort-controller: 3.0.0 + grammy: 1.37.1 + + '@grammyjs/storage-redis@2.5.1': {} + '@grammyjs/types@3.21.0': {} '@graphql-eslint/eslint-plugin@4.4.0(@types/node@24.3.0)(eslint@9.33.0(jiti@2.5.1))(graphql@16.11.0)(typescript@5.9.2)':