From 140477c87abe7a0290cd37cde4f0b92e1f4ff5d5 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Thu, 14 Aug 2025 15:15:45 +0300 Subject: [PATCH] Update dependencies for Grammy framework; add new packages for enhanced bot functionality and improve logging configuration. --- package.json | 4 +++ pnpm-lock.yaml | 42 ++++++++++++++++++++++++++++ src/bot/context.ts | 13 +++++++++ src/bot/features/download.ts | 42 ++++++++++++++++++++++++++++ src/bot/features/index.ts | 1 + src/bot/handlers/errors.ts | 12 ++++++++ src/bot/helpers/logging.ts | 20 +++++++++++++ src/bot/i18n.ts | 15 ++++++++++ src/bot/index.ts | 38 +++++++++++++++++++++++++ src/index.ts | 54 +++--------------------------------- src/utils/logger.ts | 2 ++ 11 files changed, 193 insertions(+), 50 deletions(-) create mode 100644 src/bot/context.ts create mode 100644 src/bot/features/download.ts create mode 100644 src/bot/features/index.ts create mode 100644 src/bot/handlers/errors.ts create mode 100644 src/bot/helpers/logging.ts create mode 100644 src/bot/i18n.ts create mode 100644 src/bot/index.ts diff --git a/package.json b/package.json index 4d1aa40..14e5391 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "license": "ISC", "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531", "dependencies": { + "@grammyjs/auto-chat-action": "^0.1.1", + "@grammyjs/hydrate": "^1.4.1", "@grammyjs/i18n": "^1.1.2", + "@grammyjs/parse-mode": "^2.2.0", + "@grammyjs/types": "^3.21.0", "@tobyg74/tiktok-api-dl": "^1.3.4", "@types/node": "^24.2.1", "grammy": "^1.37.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f9e5ea..a95491c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,21 @@ importers: .: dependencies: + '@grammyjs/auto-chat-action': + specifier: ^0.1.1 + version: 0.1.1(grammy@1.37.0) + '@grammyjs/hydrate': + specifier: ^1.4.1 + version: 1.4.1(grammy@1.37.0) '@grammyjs/i18n': specifier: ^1.1.2 version: 1.1.2(grammy@1.37.0) + '@grammyjs/parse-mode': + specifier: ^2.2.0 + version: 2.2.0(grammy@1.37.0) + '@grammyjs/types': + specifier: ^3.21.0 + version: 3.21.0 '@tobyg74/tiktok-api-dl': specifier: ^1.3.4 version: 1.3.4(@types/node@24.2.1)(canvas@3.1.2)(typescript@5.9.2) @@ -403,12 +415,29 @@ packages: resolution: {integrity: sha512-YF4gZ4sLYRQfctpUR2uhb5UyPUYY5n/bi3OaED/Q4awKjPjlaF8tInO3uja7pnLQcmLTURkZL7L9zxv2Z5NDwg==} engines: {node: '>=12.0.0', npm: '>=7.0.0'} + '@grammyjs/auto-chat-action@0.1.1': + resolution: {integrity: sha512-70Jdy+keIri4452tluaZpKtHag5gD8pNczoXqajsnsx7pJC5wg3DAQ5unpt0xJb8KsVBAGWuJb298SBl3TVecg==} + peerDependencies: + grammy: ^1.0.0 + + '@grammyjs/hydrate@1.4.1': + resolution: {integrity: sha512-xvd7XXoVHEhxBV2M8UHDRYkZlv32JioHfs+0l+eN34mkJJJBchuRDgRY3fkVJko8o1hGFHHLAJJTlEs2OAMolA==} + engines: {node: ^12.20.0 || >=14.13.1} + peerDependencies: + grammy: ^1.20.1 + '@grammyjs/i18n@1.1.2': resolution: {integrity: sha512-PcK06mxuDDZjxdZ5HywBhr+erEITsR816KP4DNIDDds1jpA45pfz/nS9FdZmzF8H6lMyPix3mV5WL1rT4q+BuA==} engines: {node: '>=12'} peerDependencies: grammy: ^1.10.0 + '@grammyjs/parse-mode@2.2.0': + resolution: {integrity: sha512-sI5xjXYn1ihEEf1bJx4ew2KPsX1O3jsd2V/MpA1CX2tCYlxquidr7agk4IOR5bGEK38pyNVxVBdyCiy/eMxEfQ==} + engines: {node: '>=14.13.1'} + peerDependencies: + grammy: ^1.36.1 + '@grammyjs/types@3.21.0': resolution: {integrity: sha512-IMj0EpmglPCICuyfGRx4ENKPSuzS2xMSoPgSPzHC6FtnWKDEmJLBP/GbPv/h3TAeb27txqxm/BUld+gbJk6ccQ==} @@ -3816,6 +3845,15 @@ snapshots: '@fluent/langneg@0.6.2': {} + '@grammyjs/auto-chat-action@0.1.1(grammy@1.37.0)': + dependencies: + grammy: 1.37.0 + + '@grammyjs/hydrate@1.4.1(grammy@1.37.0)': + dependencies: + abort-controller: 3.0.0 + grammy: 1.37.0 + '@grammyjs/i18n@1.1.2(grammy@1.37.0)': dependencies: '@deno/shim-deno': 0.18.2 @@ -3823,6 +3861,10 @@ snapshots: '@fluent/langneg': 0.6.2 grammy: 1.37.0 + '@grammyjs/parse-mode@2.2.0(grammy@1.37.0)': + dependencies: + grammy: 1.37.0 + '@grammyjs/types@3.21.0': {} '@graphql-eslint/eslint-plugin@4.4.0(@types/node@24.2.1)(eslint@9.33.0(jiti@2.5.1))(graphql@16.11.0)(typescript@5.9.2)': diff --git a/src/bot/context.ts b/src/bot/context.ts new file mode 100644 index 0000000..304fb3f --- /dev/null +++ b/src/bot/context.ts @@ -0,0 +1,13 @@ +import { logger } from '@/utils/logger'; +import { HydrateFlavor } from '@grammyjs/hydrate'; +import { I18nFlavor } from '@grammyjs/i18n'; +import { Context as DefaultContext } from 'grammy'; +import type { AutoChatActionFlavor } from '@grammyjs/auto-chat-action'; + +export type Context = HydrateFlavor< + DefaultContext & + AutoChatActionFlavor & + I18nFlavor & { + logger: typeof logger; + } +>; diff --git a/src/bot/features/download.ts b/src/bot/features/download.ts new file mode 100644 index 0000000..16ccab3 --- /dev/null +++ b/src/bot/features/download.ts @@ -0,0 +1,42 @@ +import { validateTikTokUrl } from '@/utils/urls'; +import type { Context } from '../context'; +import { logHandle } from '../helpers/logging'; +import { Composer, InputFile } from 'grammy'; +import { Downloader } from '@tobyg74/tiktok-api-dl'; + +const composer = new Composer(); + +const feature = composer.chatType('private'); + +feature.on('message:text', logHandle('download-message'), async (ctx) => { + try { + const url = ctx.message.text; + + if (!validateTikTokUrl(url)) return ctx.reply(ctx.t('invalid_url')); + + const { result, message } = await Downloader(url, { version: 'v3' }); + + if (message) throw new Error(message); + + const videoUrl = result?.videoHD || result?.videoSD || result?.videoWatermark; + const imagesUrls = result?.images; + + if (!videoUrl && !imagesUrls?.length) { + return ctx.reply(ctx.t('invalid_download_urls')); + } + + if (result?.type === 'video' && videoUrl) { + return ctx.replyWithVideo(new InputFile({ url: videoUrl })); + } + + if (result?.type === 'image' && imagesUrls) { + return ctx.replyWithMediaGroup(imagesUrls.map((image) => ({ media: image, type: 'photo' }))); + } + } catch (error) { + ctx.logger.error(error); + + return ctx.reply(ctx.t('generic')); + } +}); + +export { composer as download }; diff --git a/src/bot/features/index.ts b/src/bot/features/index.ts new file mode 100644 index 0000000..d1d64f3 --- /dev/null +++ b/src/bot/features/index.ts @@ -0,0 +1 @@ +export * from './download'; \ No newline at end of file diff --git a/src/bot/handlers/errors.ts b/src/bot/handlers/errors.ts new file mode 100644 index 0000000..564de39 --- /dev/null +++ b/src/bot/handlers/errors.ts @@ -0,0 +1,12 @@ +import type { Context } from '../context'; +import type { ErrorHandler } from 'grammy'; +import { getUpdateInfo } from '../helpers/logging'; + +export const errorHandler: ErrorHandler = (error) => { + const { ctx } = error; + + ctx.logger.error({ + err: error.error, + update: getUpdateInfo(ctx), + }); +}; diff --git a/src/bot/helpers/logging.ts b/src/bot/helpers/logging.ts new file mode 100644 index 0000000..1959efb --- /dev/null +++ b/src/bot/helpers/logging.ts @@ -0,0 +1,20 @@ +import type { Context } from '../context'; +import type { Update } from '@grammyjs/types'; +import type { Middleware } from 'grammy'; + +export function getUpdateInfo(ctx: Context): Omit { + const { update_id, ...update } = ctx.update; + + return update; +} + +export function logHandle(id: string): Middleware { + return (ctx, next) => { + ctx.logger.info({ + msg: `Handle "${id}"`, + ...(id.startsWith('unhandled') ? { update: getUpdateInfo(ctx) } : {}), + }); + + return next(); + }; +} diff --git a/src/bot/i18n.ts b/src/bot/i18n.ts new file mode 100644 index 0000000..14a9729 --- /dev/null +++ b/src/bot/i18n.ts @@ -0,0 +1,15 @@ +import type { Context } from './context'; +import path from 'node:path'; +import process from 'node:process'; +import { I18n } from '@grammyjs/i18n'; + +export const i18n = new I18n({ + defaultLocale: 'en', + directory: path.resolve(process.cwd(), 'locales'), + useSession: true, + fluentBundleOptions: { + useIsolating: false, + }, +}); + +export const isMultipleLocales = i18n.locales.length > 1; diff --git a/src/bot/index.ts b/src/bot/index.ts new file mode 100644 index 0000000..9da69ec --- /dev/null +++ b/src/bot/index.ts @@ -0,0 +1,38 @@ +import { Bot } from 'grammy'; +import { logger } from '@/utils/logger'; +import { Context } from './context'; +import { i18n } from './i18n'; +import { errorHandler } from './handlers/errors'; +import * as features from './features'; +import { hydrate } from '@grammyjs/hydrate'; +import { autoChatAction } from '@grammyjs/auto-chat-action'; + +type Params = { + token: string; + apiRoot: string; +}; + +export function createBot({ token, apiRoot }: Params) { + const bot = new Bot(token, { + client: { + apiRoot, + }, + }); + + bot.use(async (ctx, next) => { + ctx.logger = logger.child({ + update_id: ctx.update.update_id, + }); + + await next(); + }); + + const protectedBot = bot.errorBoundary(errorHandler); + + protectedBot.use(i18n); + protectedBot.use(autoChatAction(bot.api)); + protectedBot.use(hydrate()); + protectedBot.use(features.download); + + return bot; +} diff --git a/src/index.ts b/src/index.ts index 66b8215..d2254e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,56 +1,10 @@ +import { createBot } from './bot'; import { env } from './config/env'; -import { Downloader } from '@tobyg74/tiktok-api-dl'; -import { Bot, Context, InputFile } from 'grammy'; import { logger } from './utils/logger'; -import { validateTikTokUrl } from './utils/urls'; -import { I18n, I18nFlavor } from '@grammyjs/i18n'; -type MyContext = Context & I18nFlavor; - -const bot = new Bot(env.BOT_TOKEN, { - client: { - apiRoot: env.TELEGRAM_API_ROOT, - }, -}); - -const i18n = new I18n({ - defaultLocale: 'en', // see below for more information - directory: 'locales', // Load all translation files from locales/. -}); - -bot.use(i18n); - -bot.on('message:text', async (ctx) => { - try { - const url = ctx.message.text; - - if (!validateTikTokUrl(url)) return ctx.reply(ctx.t('invalid_url')); - - const { result, message } = await Downloader(url, { version: 'v3' }); - - if (message) throw new Error(message); - - const videoUrl = result?.videoHD || result?.videoSD || result?.videoWatermark; - const imagesUrls = result?.images; - - if (!videoUrl && !imagesUrls?.length) { - return ctx.reply(ctx.t('invalid_download_urls')); - } - - if (result?.type === 'video' && videoUrl) { - await ctx.replyWithChatAction('upload_video'); - return ctx.replyWithVideo(new InputFile({ url: videoUrl })); - } - - if (result?.type === 'image' && imagesUrls) { - await ctx.replyWithChatAction('upload_photo'); - return ctx.replyWithMediaGroup(imagesUrls.map((image) => ({ media: image, type: 'photo' }))); - } - } catch (error) { - logger.error(error); - - return ctx.reply(ctx.t('generic')); - } +const bot = createBot({ + token: env.BOT_TOKEN, + apiRoot: env.TELEGRAM_API_ROOT, }); // Stopping the bot when the Node.js process diff --git a/src/utils/logger.ts b/src/utils/logger.ts index e6c6ebb..6ee4a23 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -3,8 +3,10 @@ import pino from 'pino'; export const logger = pino({ transport: { target: 'pino-pretty', + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', options: { colorize: true, + translateTime: true, }, }, });