From 6e9d2e9b9e6f7de3e93f59111c9640e79a237422 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Wed, 14 Jan 2026 17:31:07 +0300 Subject: [PATCH] feat: enhance download feature with caption support for Instagram --- apps/bot/src/bot/features/download.ts | 46 ++++++++++++++++++++++++--- apps/bot/src/utils/instagram.ts | 13 +++----- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/apps/bot/src/bot/features/download.ts b/apps/bot/src/bot/features/download.ts index b1a1923..cc19b6e 100644 --- a/apps/bot/src/bot/features/download.ts +++ b/apps/bot/src/bot/features/download.ts @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/cognitive-complexity */ /* eslint-disable consistent-return */ import { type Context } from '../context'; import { logHandle } from '../helpers/logging'; @@ -7,6 +8,7 @@ import { getRedisInstance } from '@/utils/redis'; import { getTiktokDownloadUrl } from '@/utils/tiktok'; import { validateInstagramUrl, validateTikTokUrl, validateYoutubeUrl } from '@/utils/urls'; import { getYoutubeDownloadUrl } from '@/utils/youtube'; +import { expandableBlockquote, fmt } from '@grammyjs/parse-mode'; import { Composer, InputFile } from 'grammy'; import { cluster } from 'radashi'; @@ -15,6 +17,10 @@ const feature = composer.chatType('private'); const redis = getRedisInstance(); +function formatCaption(caption: string) { + return fmt`${expandableBlockquote} ${caption} ${expandableBlockquote}`; +} + feature.on('message:text', logHandle('download-message'), async (context) => { const url = context.message.text.trim(); @@ -29,22 +35,36 @@ feature.on('message:text', logHandle('download-message'), async (context) => { } const cachedFileId = await redis.get(url); + let cachedMessageId: number | undefined; if (cachedFileId) { - return context.replyWithVideo(cachedFileId); + const cachedMessage = await context.replyWithVideo(cachedFileId); + cachedMessageId = cachedMessage.message_id; + } + + const cachedCaption = await redis.get(`caption:${url}`); + if (cachedCaption) { + const { entities, text } = formatCaption(cachedCaption); + return context.reply(text, { + entities, + reply_parameters: cachedMessageId ? { message_id: cachedMessageId } : undefined, + }); } let imagesUrls: string[] | undefined; let videoUrl: string | undefined; + let caption: string | undefined; try { if (isTikTok) { const result = await getTiktokDownloadUrl(url); imagesUrls = result.images; videoUrl = result.play; + caption = result.title; } else if (isInstagram) { const result = await getInstagramDownloadUrl(url); imagesUrls = result.images; videoUrl = result.play; + caption = result.caption; } else if (isYoutube) { const result = await getYoutubeDownloadUrl(url); videoUrl = result.play; @@ -63,21 +83,37 @@ feature.on('message:text', logHandle('download-message'), async (context) => { return context.reply(context.t('err-invalid-download-urls')); } + let contentMessageId: number | undefined; + if (imagesUrls?.length) { const chunks = cluster(imagesUrls, 10); for (const chunk of chunks) { - await context.replyWithMediaGroup( + const imageMessages = await context.replyWithMediaGroup( chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })), ); - } - return; + if (!contentMessageId && imageMessages.length) { + contentMessageId = imageMessages.at(0)?.message_id; + } + } } if (videoUrl) { - const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl })); + const { video, ...videoMessage } = await context.replyWithVideo( + new InputFile({ url: videoUrl }), + ); + contentMessageId = videoMessage.message_id; await redis.set(url, video.file_id, 'EX', TTL_URLS); } + + if (caption) { + const { entities, text } = formatCaption(caption); + await redis.set(`caption:${url}`, caption, 'EX', TTL_URLS); + await context.reply(text, { + entities, + reply_parameters: contentMessageId ? { message_id: contentMessageId } : undefined, + }); + } }); export { composer as download }; diff --git a/apps/bot/src/utils/instagram.ts b/apps/bot/src/utils/instagram.ts index f264e81..6304e56 100644 --- a/apps/bot/src/utils/instagram.ts +++ b/apps/bot/src/utils/instagram.ts @@ -14,6 +14,7 @@ export type CarouselItem = { export type Root = { authorInfo: AuthorInfo; + caption: string; carouselItems: CarouselItem[]; id: number; mediaUrls: string[]; @@ -31,15 +32,9 @@ export async function getInstagramDownloadUrl(url: string) { const isVideo = data.type === 'video' || !data.carouselItems.length; - if (isVideo) { - return { - images: [], - play: data.mediaUrls.at(0), - }; - } - return { - images: data.mediaUrls, - play: undefined, + caption: data.caption, + images: isVideo ? undefined : data.mediaUrls, + play: isVideo ? data.mediaUrls.at(0) : undefined, }; }