Skip to content

Commit f209ca6

Browse files
committed
feat: Instant Download supporting fb and yt shorts
1 parent f0fa385 commit f209ca6

5 files changed

Lines changed: 178 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Canis supports multiple AI providers out of the box:
2626
- Resent unsend andor edit messages
2727
- Automatic Call rejection
2828
- Dynamic Commands Loading
29+
- Instant Download of Videos from supported platform
2930
- Commands built here are compatible to used in canis telegram version
3031
- Lots of lots of commands to keep the group interesting
3132
- Integrated with Phishtank & Virustotal to keep the group safe and sound

src/components/events/message.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { containsAny } from "../utils/string";
1919
import { phishingSet } from "../../index";
2020
import { getSetting } from "../services/settings";
2121
import { normalize } from "../utils/url";
22+
import { InstantDownloader } from "../utils/instantdl/downloader";
2223

2324
const regex = emojiRegex();
2425
const commandPrefix = process.env.COMMAND_PREFIX || "!";
@@ -38,6 +39,19 @@ const mentionResponses = [
3839
"Did you just @ me for vibes, or do I owe you money? 💸",
3940
];
4041

42+
async function isRateLimit(msg: Message, lid: string) {
43+
if (msg.fromMe) return false;
44+
45+
const rate = await rateLimiter(lid);
46+
if (rate) return true;
47+
if (rate === null) {
48+
await msg.reply("Please wait a minute or so.");
49+
return true;
50+
}
51+
52+
return false;
53+
}
54+
4155
export default async function (msg: Message, type: string) {
4256
// ignore message if it is older than 10 seconds
4357
if (msg.timestamp < Date.now() / 1000 - 10 && type === "create") return;
@@ -86,6 +100,19 @@ export default async function (msg: Message, type: string) {
86100

87101
if (msg.isForwarded) return;
88102

103+
Promise.resolve().then(async () => {
104+
if (await isRateLimit(msg, lid)) return;
105+
106+
const extractUrls = msg.body.match(/(https?:\/\/[^\s]+)/g);
107+
if (!extractUrls) return;
108+
109+
const url = extractUrls[Math.floor(Math.random() * extractUrls.length)];
110+
const message = msg;
111+
message.body = url;
112+
log.info("Instant Downloader", `Found ${url}`);
113+
await InstantDownloader(message);
114+
});
115+
89116
// process normalization
90117
msg.body = msg.body
91118
.normalize("NFKC")
@@ -98,6 +125,8 @@ export default async function (msg: Message, type: string) {
98125
*/
99126
if (msg.hasQuotedMsg) {
100127
Promise.resolve().then(async () => {
128+
if (await isRateLimit(msg, lid)) return;
129+
101130
const quoted = await msg.getQuotedMessage();
102131
await quiz(msg, quoted);
103132
});
@@ -185,14 +214,7 @@ export default async function (msg: Message, type: string) {
185214
/*
186215
* Rate limit commands to prevent abuse.
187216
*/
188-
if (!msg.fromMe) {
189-
const rate = await rateLimiter(lid);
190-
if (rate) return;
191-
if (rate === null) {
192-
await msg.reply("Please wait a minute or so.");
193-
return;
194-
}
195-
}
217+
if (await isRateLimit(msg, lid)) return;
196218

197219
/*
198220
* Role base restrictions.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { MessageMedia } from "whatsapp-web.js";
2+
import { Message } from "../../../../types/message";
3+
import { FacebookInstantDownloader } from "./facebook";
4+
import { YoutubeShortsInstantDownloader } from "./youtube";
5+
6+
export interface Video {
7+
video: MessageMedia;
8+
title: string | undefined;
9+
}
10+
11+
const facebookUrlRegex =
12+
/^(https?:\/\/)?(www\.)?(facebook\.com|fb\.watch)\/[^\s]+$/i;
13+
const youtubeShortsUrlRegex =
14+
/^(https?:\/\/)?(www\.)?youtube\.com\/shorts\/[A-Za-z0-9_-]{11}$/i;
15+
const tiktokUrlRegex =
16+
/^(https?:\/\/)?(www\.)?(tiktok\.com\/(@[A-Za-z0-9._-]+\/video\/\d+|t\/[A-Za-z0-9]+)|vm\.tiktok\.com\/[A-Za-z0-9]+)\/?$/i;
17+
18+
export async function InstantDownloader(msg: Message) {
19+
const query = msg.body;
20+
let video: Video | undefined;
21+
22+
if (facebookUrlRegex.test(query)) {
23+
video = await FacebookInstantDownloader(query);
24+
} else if (youtubeShortsUrlRegex.test(query)) {
25+
video = await YoutubeShortsInstantDownloader(query);
26+
} else if (tiktokUrlRegex.test(query)) {
27+
}
28+
29+
if (!video) return;
30+
31+
await msg.reply(video.video, undefined, {
32+
caption: video.title ? video.title : "Instant Download",
33+
});
34+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { MessageMedia } from "whatsapp-web.js";
2+
import { getFbVideoInfo } from "fb-downloader-scrapper";
3+
import crypto from "crypto";
4+
import log from "../log";
5+
import axios from "../../axios";
6+
import fs from "fs";
7+
import { Video } from "./downloader";
8+
9+
const fileExists = async (filePath: string) => {
10+
try {
11+
await fs.promises.access(filePath, fs.constants.F_OK);
12+
return true;
13+
} catch {
14+
return false;
15+
}
16+
};
17+
18+
function md5FromUrl(url: string) {
19+
return crypto.createHash("md5").update(url).digest("hex");
20+
}
21+
22+
export async function FacebookInstantDownloader(
23+
query: string,
24+
): Promise<Video | undefined> {
25+
const result = await getFbVideoInfo(query);
26+
if (!result.url) return undefined;
27+
28+
const tempDir = "./.temp";
29+
fs.mkdirSync(tempDir, { recursive: true });
30+
const tempPath = `${tempDir}/${md5FromUrl(result.url)}.mp4`;
31+
32+
if (await fileExists(tempPath)) {
33+
return {
34+
video: MessageMedia.fromFilePath(tempPath),
35+
title: result.title,
36+
};
37+
}
38+
39+
const response = await axios.get(result.hd || result.sd, {
40+
responseType: "arraybuffer",
41+
});
42+
fs.writeFileSync(tempPath, response.data);
43+
44+
return {
45+
video: MessageMedia.fromFilePath(tempPath),
46+
title: result.title,
47+
};
48+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { MessageMedia } from "whatsapp-web.js";
2+
import fs from "fs";
3+
import path from "path";
4+
import { Innertube, UniversalCache, Utils } from "youtubei.js";
5+
import log from "../log";
6+
import { Video } from "./downloader";
7+
8+
const fileExists = async (filePath: string) => {
9+
try {
10+
await fs.promises.access(filePath, fs.constants.F_OK);
11+
return true;
12+
} catch {
13+
return false;
14+
}
15+
};
16+
17+
export async function YoutubeShortsInstantDownloader(
18+
query: string,
19+
): Promise<Video | undefined> {
20+
const yt = await Innertube.create({
21+
cache: new UniversalCache(false),
22+
generate_session_locally: true,
23+
player_id: "0004de42",
24+
});
25+
26+
const endpoint = await yt.resolveURL(query);
27+
const videoId = endpoint.payload.videoId;
28+
if (!videoId) return undefined;
29+
30+
const tempDir = "./.temp";
31+
await fs.promises.mkdir(tempDir, { recursive: true });
32+
const tempPath = path.join(tempDir, `${videoId}.mp4`);
33+
34+
if (await fileExists(tempPath)) {
35+
return {
36+
video: MessageMedia.fromFilePath(tempPath),
37+
title: undefined,
38+
};
39+
}
40+
41+
const stream = await yt.download(videoId, {
42+
type: "video+audio",
43+
quality: "best",
44+
format: "mp4",
45+
});
46+
47+
if (!stream) return undefined;
48+
49+
let writeStream = fs.createWriteStream(tempPath);
50+
51+
for await (const chunk of Utils.streamToIterable(stream)) {
52+
writeStream.write(chunk);
53+
}
54+
55+
await new Promise<void>((resolve, reject) => {
56+
writeStream.end();
57+
writeStream.on("finish", resolve);
58+
writeStream.on("error", reject);
59+
});
60+
61+
return {
62+
video: MessageMedia.fromFilePath(tempPath),
63+
title: undefined,
64+
};
65+
}

0 commit comments

Comments
 (0)