feat: refactor Instagram download URL handling to simplify video response structure

This commit is contained in:
vchikalkin 2026-01-14 18:18:20 +03:00
parent f48230cbeb
commit ebff5a7ce1
2 changed files with 114 additions and 55 deletions

View File

@ -1,4 +1,3 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable consistent-return */
import { type Context } from '../context';
import { logHandle } from '../helpers/logging';
@ -9,7 +8,7 @@ import { removeHashtags } from '@/utils/text';
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 { code, expandableBlockquote, fmt } from '@grammyjs/parse-mode';
import { Composer, InputFile } from 'grammy';
import { cluster } from 'radashi';
@ -17,10 +16,103 @@ const composer = new Composer<Context>();
const feature = composer.chatType('private');
const redis = getRedisInstance();
// Проверить кэш и, при наличии, ответить видеo/подписью.
// Возвращает { contentMessageId?, captionSent? }
async function checkCacheAndReply(context: Context, url: string) {
let contentMessageId: number | undefined;
const cachedVideoId = await redis.get(url);
if (cachedVideoId) {
const cachedMessage = await context.replyWithVideo(cachedVideoId);
contentMessageId = cachedMessage.message_id;
}
if (contentMessageId) {
const cachedCaption = await redis.get(`caption:${url}`);
if (cachedCaption) {
const { entities, text } = formatCaption(cachedCaption);
if (text.trim().length)
await context.reply(text, {
entities,
reply_parameters: contentMessageId ? { message_id: contentMessageId } : undefined,
});
return { captionSent: true, contentMessageId };
}
}
return { contentMessageId };
}
// Форматирование подписи как expandable blockquote
function formatCaption(caption: string) {
const cleanCaption = removeHashtags(caption);
return fmt`${expandableBlockquote} ${cleanCaption} ${expandableBlockquote}`;
return fmt`${expandableBlockquote} ${code} ${cleanCaption} ${code} ${expandableBlockquote}`;
}
// Отправка подписи и запись в кэш
async function sendCaptionAndCache(
context: Context,
caption: string | undefined,
url: string,
contentMessageId?: number,
) {
if (!caption) return;
const { entities, text } = formatCaption(caption);
await redis.set(`caption:${url}`, caption, 'EX', TTL_URLS);
if (text.trim().length)
await context.reply(text, {
entities,
reply_parameters: contentMessageId ? { message_id: contentMessageId } : undefined,
});
}
// Отправка изображений (порциями). Возвращает contentMessageId (если установлен)
async function sendImages(
context: Context,
imagesUrls: string[],
existingContentMessageId?: number,
) {
if (!imagesUrls?.length) return existingContentMessageId;
const chunks = cluster(imagesUrls, 10);
let contentMessageId = existingContentMessageId;
for (const chunk of chunks) {
const imageMessages = await context.replyWithMediaGroup(
chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })),
);
if (!contentMessageId && imageMessages.length) {
contentMessageId = imageMessages.at(0)?.message_id;
}
}
return contentMessageId;
}
// Отправка видео и запись в кэш (только если видео отправлено впервые)
async function sendVideoAndCache(
context: Context,
videoUrl: string | undefined,
url: string,
existingContentMessageId?: number,
) {
let contentMessageId = existingContentMessageId;
if (videoUrl && !contentMessageId) {
const { video, ...videoMessage } = await context.replyWithVideo(
new InputFile({ url: videoUrl }),
);
contentMessageId = videoMessage.message_id;
// сохраняем file_id полученного видео
await redis.set(url, video.file_id, 'EX', TTL_URLS);
}
return contentMessageId;
}
feature.on('message:text', logHandle('download-message'), async (context) => {
@ -37,25 +129,10 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
return context.reply(context.t('err-invalid-url'));
}
let contentMessageId: number | undefined;
// Проверка кеша
const cachedVideoId = await redis.get(url);
if (cachedVideoId) {
const cachedMessage = await context.replyWithVideo(cachedVideoId);
contentMessageId = cachedMessage.message_id;
}
if (contentMessageId) {
const cachedCaption = await redis.get(`caption:${url}`);
if (cachedCaption) {
const { entities, text } = formatCaption(cachedCaption);
return context.reply(text, {
entities,
reply_parameters: contentMessageId ? { message_id: contentMessageId } : undefined,
});
}
}
// Проверка кеша и быстрый ответ, если есть подпись в кеше
const cacheResult = await checkCacheAndReply(context, url);
if (cacheResult.captionSent) return;
let contentMessageId = cacheResult.contentMessageId;
// Загрузка данных с сервисов
let imagesUrls: string[] | undefined;
@ -91,39 +168,13 @@ feature.on('message:text', logHandle('download-message'), async (context) => {
}
// Отправка изображений
if (imagesUrls?.length) {
const chunks = cluster(imagesUrls, 10);
for (const chunk of chunks) {
const imageMessages = await context.replyWithMediaGroup(
chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })),
);
contentMessageId = await sendImages(context, imagesUrls ?? [], contentMessageId);
if (!contentMessageId && imageMessages.length) {
contentMessageId = imageMessages.at(0)?.message_id;
}
}
}
// Отправка видео (если ещё не отправлено) и запись в кэш
contentMessageId = await sendVideoAndCache(context, videoUrl, url, contentMessageId);
// Отправка видео
if (videoUrl && !contentMessageId) {
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,
});
}
// Отправка описания и запись в кэш
await sendCaptionAndCache(context, caption, url, contentMessageId);
});
export { composer as download };

View File

@ -32,9 +32,17 @@ export async function getInstagramDownloadUrl(url: string) {
const isVideo = data.type === 'video' || !data.carouselItems.length;
if (isVideo) {
return {
caption: data.caption,
images: [],
play: data.mediaUrls.at(0),
};
}
return {
caption: data.caption,
images: isVideo ? undefined : data.mediaUrls,
play: isVideo ? data.mediaUrls.at(0) : undefined,
images: data.mediaUrls,
play: undefined,
};
}