diff --git a/apps/bot/package.json b/apps/bot/package.json index defd476..a5ca0fd 100644 --- a/apps/bot/package.json +++ b/apps/bot/package.json @@ -27,8 +27,9 @@ "@tobyg74/tiktok-api-dl": "^1.3.4", "@types/node": "catalog:", "grammy": "^1.37.0", - "pino-pretty": "^13.1.1", + "ioredis": "^5.7.0", "pino": "^9.9.0", + "pino-pretty": "^13.1.1", "tsup": "^8.5.0", "typescript": "catalog:", "zod": "catalog:" diff --git a/apps/bot/src/bot/features/download.ts b/apps/bot/src/bot/features/download.ts index 749ab05..46cac38 100644 --- a/apps/bot/src/bot/features/download.ts +++ b/apps/bot/src/bot/features/download.ts @@ -1,6 +1,8 @@ /* eslint-disable consistent-return */ import { type Context } from '../context'; import { logHandle } from '../helpers/logging'; +import { TTL } from '@/config/redis'; +import { createRedisInstance } from '@/utils/redis'; import { validateTikTokUrl } from '@/utils/urls'; import { Downloader } from '@tobyg74/tiktok-api-dl'; import { Composer, InputFile } from 'grammy'; @@ -9,12 +11,18 @@ const composer = new Composer(); const feature = composer.chatType('private'); +const redis = createRedisInstance(); + feature.on('message:text', logHandle('download-message'), async (context) => { try { - const url = context.message.text; + const url = context.message.text.trim(); if (!validateTikTokUrl(url)) return context.reply(context.t('invalid_url')); + const cachedFileId = await redis.get(url); + + if (cachedFileId) return context.replyWithVideo(cachedFileId); + const { message, result } = await Downloader(url, { version: 'v3' }); if (message) throw new Error(message); @@ -27,7 +35,9 @@ feature.on('message:text', logHandle('download-message'), async (context) => { } if (result?.type === 'video' && videoUrl) { - return context.replyWithVideo(new InputFile({ url: videoUrl })); + const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl })); + + await redis.set(url, video.file_id, 'EX', TTL); } if (result?.type === 'image' && imagesUrls) { diff --git a/apps/bot/src/config/env.ts b/apps/bot/src/config/env.ts index 0308a11..5e6c92a 100644 --- a/apps/bot/src/config/env.ts +++ b/apps/bot/src/config/env.ts @@ -3,6 +3,12 @@ import { z } from 'zod'; export const envSchema = z.object({ BOT_TOKEN: z.string(), + REDIS_HOST: z.string(), + REDIS_PASSWORD: z.string(), + REDIS_PORT: z + .string() + .transform((value) => Number.parseInt(value, 10)) + .default('6379'), TELEGRAM_API_ROOT: z.string(), }); diff --git a/apps/bot/src/config/redis.ts b/apps/bot/src/config/redis.ts new file mode 100644 index 0000000..0150fcd --- /dev/null +++ b/apps/bot/src/config/redis.ts @@ -0,0 +1 @@ +export const TTL = 12 * 60 * 60; // 12 hours in seconds diff --git a/apps/bot/src/utils/redis.ts b/apps/bot/src/utils/redis.ts new file mode 100644 index 0000000..4a98de9 --- /dev/null +++ b/apps/bot/src/utils/redis.ts @@ -0,0 +1,15 @@ +import { env } from '@/config/env'; +import { logger } from '@/utils/logger'; +import Redis from 'ioredis'; + +export function createRedisInstance() { + const redis = new Redis({ + host: env.REDIS_HOST, + password: env.REDIS_PASSWORD, + port: env.REDIS_PORT, + }); + + redis.on('error', logger.error); + + return redis; +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 750d43b..45f938a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,4 +1,29 @@ services: + redis: + env_file: + - .env + image: redis:8-alpine + restart: always + ports: + - "127.0.0.1:6379:6379" + volumes: + - redis-data:/data + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"] + deploy: + resources: + limits: + cpus: '0.50' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + next-downloader-bot: env_file: - .env @@ -6,6 +31,9 @@ services: context: . dockerfile: ./apps/bot/Dockerfile restart: always + depends_on: + - redis + telegram-bot-api: env_file: - .env @@ -15,8 +43,9 @@ services: # TELEGRAM_API_HASH: "" volumes: - telegram-bot-api-data:/var/lib/telegram-bot-api -# ports: -# - "8081:8081" + ports: + - "127.0.0.1:8081:8081" volumes: telegram-bot-api-data: + redis-data: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34d0290..8654c46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: grammy: specifier: ^1.37.0 version: 1.37.1 + ioredis: + specifier: ^5.7.0 + version: 5.7.0 pino: specifier: ^9.9.0 version: 9.9.0 @@ -673,6 +676,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@ioredis/commands@1.3.0': + resolution: {integrity: sha512-M/T6Zewn7sDaBQEqIZ8Rb+i9y8qfGmq+5SDFSf9sA2lUZTmdDLVdOiQaeDp+Q4wElZ9HG1GAX5KhDaidp6LQsQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1331,6 +1337,10 @@ packages: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1499,6 +1509,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + detect-libc@2.0.4: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} @@ -2367,6 +2381,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ioredis@5.7.0: + resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + engines: {node: '>=12.22.0'} + ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -2657,10 +2675,16 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.lowercase@4.3.0: resolution: {integrity: sha512-UcvP1IZYyDKyEL64mmrwoA1AbFu5ahojhTtkOUr1K9dbuxzS9ev8i4TxMMGCqRC9TE8uDaSoufNAXxRPNTseVA==} @@ -3180,6 +3204,14 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3422,6 +3454,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4501,6 +4536,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@ioredis/commands@1.3.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5201,6 +5238,8 @@ snapshots: slice-ansi: 5.0.0 string-width: 7.2.0 + cluster-key-slot@1.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5348,6 +5387,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + detect-libc@2.0.4: {} didyoumean@1.2.2: {} @@ -6465,6 +6506,20 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ioredis@5.7.0: + dependencies: + '@ioredis/commands': 1.3.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ip-address@10.0.1: {} is-array-buffer@3.0.5: @@ -6778,8 +6833,12 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + lodash.get@4.4.2: {} + lodash.isarguments@3.1.0: {} + lodash.lowercase@4.3.0: {} lodash.merge@4.6.2: {} @@ -7281,6 +7340,12 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.1 @@ -7562,6 +7627,8 @@ snapshots: stable-hash@0.0.5: {} + standard-as-callback@2.1.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0