Compare commits

...

2 Commits

Author SHA1 Message Date
vchikalkin
048779cfeb fix download from tiktok 2025-09-12 14:31:53 +03:00
vchikalkin
9a3b3d6bad refactor: global error handling 2025-09-12 13:58:26 +03:00
5 changed files with 114 additions and 806 deletions

View File

@ -28,12 +28,13 @@
"@grammyjs/storage-redis": "^2.5.1",
"@grammyjs/types": "^3.21.0",
"@repo/typescript-config": "workspace:*",
"@tobyg74/tiktok-api-dl": "^1.3.4",
"@types/node": "catalog:",
"axios": "^1.12.0",
"grammy": "^1.37.0",
"ioredis": "^5.7.0",
"pino": "^9.9.0",
"pino-pretty": "^13.1.1",
"radashi": "^12.6.2",
"tsup": "^8.5.0",
"typescript": "catalog:",
"zod": "catalog:"

View File

@ -3,9 +3,10 @@ import { type Context } from '../context';
import { logHandle } from '../helpers/logging';
import { TTL_URLS } from '@/config/redis';
import { getRedisInstance } from '@/utils/redis';
import { getTiktokDownloadUrl } from '@/utils/tiktok';
import { validateTikTokUrl } from '@/utils/urls';
import { Downloader } from '@tobyg74/tiktok-api-dl';
import { Composer, InputFile } from 'grammy';
import { cluster } from 'radashi';
const composer = new Composer<Context>();
const feature = composer.chatType('private');
@ -13,44 +14,37 @@ const feature = composer.chatType('private');
const redis = getRedisInstance();
feature.on('message:text', logHandle('download-message'), async (context) => {
try {
const url = context.message.text.trim();
const url = context.message.text.trim();
if (!validateTikTokUrl(url)) {
return context.reply(context.t('err-invalid-url'));
}
if (!validateTikTokUrl(url)) {
return context.reply(context.t('err-invalid-url'));
}
const cachedFileId = await redis.get(url);
if (cachedFileId) {
return context.replyWithVideo(cachedFileId);
}
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);
}
const { images: imagesUrls, play: videoUrl } = await getTiktokDownloadUrl(url);
const videoUrl = result?.videoSD || result?.videoWatermark;
const imagesUrls = result?.images;
if (!videoUrl && !imagesUrls?.length) {
return context.reply(context.t('err-invalid-download-urls'));
}
if (!videoUrl && !imagesUrls?.length) {
return context.reply(context.t('err-invalid-download-urls'));
}
if (result?.type === 'video' && videoUrl) {
const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl }));
await redis.set(url, video.file_id, 'EX', TTL_URLS);
return;
}
if (result?.type === 'image' && imagesUrls) {
return context.replyWithMediaGroup(
imagesUrls.map((image) => ({ media: image, type: 'photo' })),
if (imagesUrls?.length) {
const chunks = cluster(imagesUrls, 10);
for (const chunk of chunks) {
await context.replyWithMediaGroup(
chunk.map((imageUrl) => ({ media: imageUrl, type: 'photo' })),
);
}
} catch (error) {
context.logger.error(error);
return context.reply(context.t('err-generic'));
return;
}
if (videoUrl) {
const { video } = await context.replyWithVideo(new InputFile({ url: videoUrl }));
await redis.set(url, video.file_id, 'EX', TTL_URLS);
}
});

View File

@ -2,9 +2,11 @@ import { type Context } from '../context';
import { getUpdateInfo } from '../helpers/logging';
import { type ErrorHandler } from 'grammy';
export const errorHandler: ErrorHandler<Context> = (error) => {
export const errorHandler: ErrorHandler<Context> = async (error) => {
const { ctx } = error;
await ctx.reply(ctx.t('err-generic'));
ctx.logger.error({
err: error.error,
update: getUpdateInfo(ctx),

View File

@ -0,0 +1,45 @@
import axios from 'axios';
export type Data = {
ai_dynamic_cover: string;
anchors_extras: string;
collect_count: number;
comment_count: number;
commercial_video_info: string;
cover: string;
create_time: number;
digg_count: number;
download_count: number;
duration: number;
id: string;
images: string[];
is_ad: boolean;
item_comment_settings: number;
mentioned_users: string;
music: string;
origin_cover: string;
play: string;
play_count: number;
region: string;
share_count: number;
size: number;
title: string;
wm_size: number;
wmplay: string;
};
export type Root = {
code: number;
data: Data;
msg: string;
processed_time: number;
};
export async function getTiktokDownloadUrl(url: string) {
const res = await axios.get(`https://tikwm.com/api/?url=${encodeURIComponent(url)}`);
const { data } = res.data as Root;
if (!data) throw new Error('Invalid TikTok response');
return data;
}

810
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff