feat: enhance download feature with caption support for Instagram

This commit is contained in:
vchikalkin 2026-01-14 17:31:07 +03:00
parent ce78952fec
commit 6e9d2e9b9e
2 changed files with 45 additions and 14 deletions

View File

@ -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 };

View File

@ -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,
};
}