summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
Diffstat (limited to 'src/api')
-rw-r--r--src/api/util/handlers/Message.ts143
-rw-r--r--src/api/util/index.ts1
-rw-r--r--src/api/util/utility/EmbedHandlers.ts140
3 files changed, 156 insertions, 128 deletions
diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts
index c0bdb6b0..09b86fc2 100644
--- a/src/api/util/handlers/Message.ts
+++ b/src/api/util/handlers/Message.ts
@@ -24,9 +24,8 @@ import {
 	MessageCreateSchema,
 } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import fetch from "node-fetch";
-import cheerio from "cheerio";
 import { In } from "typeorm";
+import { EmbedHandlers } from "@fosscord/api";
 const allow_empty = false;
 // TODO: check webhook, application, system author, stickers
 // TODO: embed gifs/videos/images
@@ -34,18 +33,6 @@ const allow_empty = false;
 const LINK_REGEX =
 	/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g;
 
-const DEFAULT_FETCH_OPTIONS: any = {
-	redirect: "follow",
-	follow: 1,
-	headers: {
-		"user-agent":
-			"Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)",
-	},
-	// size: 1024 * 1024 * 5, 	// grabbed from config later
-	compress: true,
-	method: "GET",
-};
-
 export async function handleMessage(opts: MessageOptions): Promise<Message> {
 	const channel = await Channel.findOneOrFail({
 		where: { id: opts.channel_id },
@@ -200,124 +187,24 @@ export async function postHandleMessage(message: Message) {
 
 	links = links.slice(0, 20) as RegExpMatchArray; // embed max 20 links — TODO: make this configurable with instance policies
 
-	const { endpointPublic, resizeWidthMax, resizeHeightMax } =
-		Config.get().cdn;
-
 	for (const link of links) {
-		try {
-			const request = await fetch(link, {
-				...DEFAULT_FETCH_OPTIONS,
-				size: Config.get().limits.message.maxEmbedDownloadSize,
-			});
-
-			let embed: Embed;
-
-			const type = request.headers.get("content-type");
-			if (type?.indexOf("image") == 0) {
-				embed = {
-					provider: {
-						url: link,
-						name: new URL(link).hostname,
-					},
-					image: {
-						// can't be bothered rn
-						proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(
-							link,
-						)}?width=500&height=400`,
-						url: link,
-						width: 500,
-						height: 400,
-					},
-				};
-				data.embeds.push(embed);
-			} else {
-				const text = await request.text();
-				const $ = cheerio.load(text);
-
-				const title = $('meta[property="og:title"]').attr("content");
-				const provider_name = $('meta[property="og:site_name"]').text();
-				const author_name = $('meta[property="article:author"]').attr(
-					"content",
-				);
-				const description =
-					$('meta[property="og:description"]').attr("content") ||
-					$('meta[property="description"]').attr("content");
+		let embed: Embed;
+		const url = new URL(link);
 
-				const image = $('meta[property="og:image"]').attr("content");
-				const width =
-					parseInt(
-						$('meta[property="og:image:width"]').attr("content") ||
-						"",
-					) || undefined;
-				const height =
-					parseInt(
-						$('meta[property="og:image:height"]').attr("content") ||
-						"",
-					) || undefined;
+		// bit gross, but whatever!
+		const { endpointPublic } = Config.get().cdn;
+		const handler = url.hostname == new URL(endpointPublic!).hostname ? EmbedHandlers["self"] : EmbedHandlers[url.hostname] || EmbedHandlers["default"];
 
-				const url = $('meta[property="og:url"]').attr("content");
-				// TODO: color
-				embed = {
-					provider: {
-						url: link,
-						name: provider_name,
-					},
-				};
-
-				const resizeWidth = Math.min(resizeWidthMax ?? 1, width ?? 100);
-				const resizeHeight = Math.min(
-					resizeHeightMax ?? 1,
-					height ?? 100,
-				);
-				if (author_name) embed.author = { name: author_name };
-				if (image)
-					embed.thumbnail = {
-						proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(
-							image,
-						)}?width=${resizeWidth}&height=${resizeHeight}`,
-						url: image,
-						width: width,
-						height: height,
-					};
-				if (title) embed.title = title;
-				if (url) embed.url = url;
-				if (description) embed.description = description;
-
-				const approvedProviders = [
-					"media4.giphy.com",
-					"c.tenor.com",
-					// todo: make configurable? don't really care tho
-				];
-
-				// very bad code below
-				// don't care lol
-				if (
-					embed?.thumbnail?.url &&
-					approvedProviders.indexOf(
-						new URL(embed.thumbnail.url).hostname,
-					) !== -1
-				) {
-					embed = {
-						provider: {
-							url: link,
-							name: new URL(link).hostname,
-						},
-						image: {
-							proxy_url: `${endpointPublic}/external/resize/${encodeURIComponent(
-								image!,
-							)}?width=${resizeWidth}&height=${resizeHeight}`,
-							url: image,
-							width: width,
-							height: height,
-						},
-					};
-				}
+		try {
+			const res = await handler(url);
+			if (!res) continue;
+			embed = res;
+		}
+		catch (e) {
+			continue;
+		}
 
-				if (title || description) {
-					data.embeds.push(embed);
-				}
-			}
-		} catch (error) { }
+		data.embeds.push(embed);
 	}
 
 	await Promise.all([
diff --git a/src/api/util/index.ts b/src/api/util/index.ts
index 9f375f72..ffad0607 100644
--- a/src/api/util/index.ts
+++ b/src/api/util/index.ts
@@ -7,3 +7,4 @@ export * from "./handlers/route";
 export * from "./utility/String";
 export * from "./handlers/Voice";
 export * from "./utility/captcha";
+export * from "./utility/EmbedHandlers";
\ No newline at end of file
diff --git a/src/api/util/utility/EmbedHandlers.ts b/src/api/util/utility/EmbedHandlers.ts
new file mode 100644
index 00000000..d6b69d86
--- /dev/null
+++ b/src/api/util/utility/EmbedHandlers.ts
@@ -0,0 +1,140 @@
+import { Config, Embed, EmbedType } from "@fosscord/util";
+import fetch, { Response } from "node-fetch";
+import * as cheerio from "cheerio";
+import probe from "probe-image-size";
+
+export const DEFAULT_FETCH_OPTIONS: any = {
+	redirect: "follow",
+	follow: 1,
+	headers: {
+		"user-agent":
+			"Mozilla/5.0 (compatible; Fosscord/1.0; +https://github.com/fosscord/fosscord)",
+	},
+	// size: 1024 * 1024 * 5, 	// grabbed from config later
+	compress: true,
+	method: "GET",
+};
+
+export const getProxyUrl = (url: URL, width: number, height: number) => {
+	const { endpointPublic, resizeWidthMax, resizeHeightMax } = Config.get().cdn;
+	width = Math.min(width || 500, resizeWidthMax || width);
+	height = Math.min(height || 500, resizeHeightMax || width);
+	return `${endpointPublic}/external/resize/${encodeURIComponent(url.href)}?width=${width}&height=${height}`;
+};
+
+export const getMetaDescriptions = async (url: URL) => {
+	let response: Response;
+	try {
+		response = await fetch(url, {
+			...DEFAULT_FETCH_OPTIONS,
+			size: Config.get().limits.message.maxEmbedDownloadSize,
+		});
+	}
+	catch (e) {
+		return null;
+	}
+
+	const text = await response.text();
+	const $ = cheerio.load(text);
+
+	return {
+		title: $('meta[property="og:title"]').attr("content"),
+		provider_name: $('meta[property="og:site_name"]').text(),
+		author: $('meta[property="article:author"]').attr("content"),
+		description:
+			$('meta[property="og:description"]').attr("content") ||
+			$('meta[property="description"]').attr("content"),
+		image: $('meta[property="og:image"]').attr("content"),
+		width: parseInt(
+			$('meta[property="og:image:width"]').attr("content") ||
+			"",
+		) || undefined,
+		height: parseInt(
+			$('meta[property="og:image:height"]').attr("content") ||
+			"",
+		) || undefined,
+		url: $('meta[property="og:url"]').attr("content"),
+		youtube_embed: $(`meta[property="og:video:secure_url"]`).attr("content")
+	};
+};
+
+const genericImageHandler = async (url: URL): Promise<Embed | null> => {
+	const metas = await getMetaDescriptions(url);
+	if (!metas) return null;
+
+	const result = await probe(url.href);
+
+	const width = metas.width || result.width;
+	const height = metas.height || result.height;
+
+	return {
+		url: url.href,
+		type: EmbedType.image,
+		thumbnail: {
+			width: width,
+			height: height,
+			url: url.href,
+			proxy_url: getProxyUrl(url, result.width, result.height),
+		}
+	};
+};
+
+export const EmbedHandlers: { [key: string]: (url: URL) => Promise<Embed | null>; } = {
+	// the url does not have a special handler
+	"default": genericImageHandler,
+
+	"giphy.com": genericImageHandler,
+	"media4.giphy.com": genericImageHandler,
+	"tenor.com": genericImageHandler,
+	"c.tenor.com": genericImageHandler,
+	"media.tenor.com": genericImageHandler,
+
+	"www.youtube.com": async (url: URL): Promise<Embed | null> => {
+		const metas = await getMetaDescriptions(url);
+		if (!metas) return null;
+
+		return {
+			video: {
+				// TODO: does this adjust with aspect ratio?
+				width: metas.width,
+				height: metas.height,
+				url: metas.youtube_embed!,
+			},
+			url: url.href,
+			type: EmbedType.video,
+			title: metas.title,
+			thumbnail: {
+				width: metas.width,
+				height: metas.height,
+				url: metas.image,
+				proxy_url: getProxyUrl(new URL(metas.image!), metas.width!, metas.height!),
+			},
+			provider: {
+				url: "https://www.youtube.com",
+				name: "YouTube",
+			},
+			description: metas.description,
+			color: 16711680,
+			author: {
+				name: metas.author,
+				// TODO: author channel url
+			}
+		};
+	},
+
+	// the url is an image from this instance
+	"self": async (url: URL): Promise<Embed | null> => {
+		const result = await probe(url.href);
+
+		return {
+			url: url.href,
+			type: EmbedType.image,
+			thumbnail: {
+				width: result.width,
+				height: result.height,
+				url: url.href,
+				proxy_url: url.href,
+			}
+		};
+	},
+};
\ No newline at end of file