summary refs log tree commit diff
diff options
context:
space:
mode:
authorMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-10-01 14:44:32 +1000
committerMadeline <46743919+MaddyUnderStars@users.noreply.github.com>2022-10-01 14:44:32 +1000
commite18af893f6d504325d7a4da434f6f67e89c30286 (patch)
tree67d0fbab956aa8a2e0d95492e898509c877d9c45
parentMake `afk` optional in ActivitySchema (diff)
downloadserver-e18af893f6d504325d7a4da434f6f67e89c30286.tar.xz
Better embed handling
-rw-r--r--package-lock.json140
-rw-r--r--package.json2
-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
5 files changed, 298 insertions, 128 deletions
diff --git a/package-lock.json b/package-lock.json
index 4b19a8bf..5f7abc85 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,6 +38,7 @@
 				"node-fetch": "^2.6.7",
 				"node-os-utils": "^1.3.7",
 				"picocolors": "^1.0.0",
+				"probe-image-size": "^7.2.3",
 				"proxy-agent": "^5.0.0",
 				"sharp": "^0.31.0",
 				"sqlite3": "^5.1.1",
@@ -57,6 +58,7 @@
 				"@types/node": "^18.7.20",
 				"@types/node-fetch": "^2.6.2",
 				"@types/node-os-utils": "^1.3.0",
+				"@types/probe-image-size": "^7.2.0",
 				"@types/sharp": "^0.31.0",
 				"@types/ws": "^8.5.3",
 				"express": "^4.18.1",
@@ -1710,6 +1712,15 @@
 				"@types/express": "*"
 			}
 		},
+		"node_modules/@types/needle": {
+			"version": "2.5.3",
+			"resolved": "https://registry.npmjs.org/@types/needle/-/needle-2.5.3.tgz",
+			"integrity": "sha512-RwgTwMRaedfyCBe5SSWMpm1Yqzc5UPZEMw0eAd09OSyV93nLRj9/evMGZmgFeHKzUOd4xxtHvgtc+rjcBjI1Qg==",
+			"dev": true,
+			"dependencies": {
+				"@types/node": "*"
+			}
+		},
 		"node_modules/@types/node": {
 			"version": "18.7.20",
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.20.tgz",
@@ -1753,6 +1764,16 @@
 				"@types/node": "*"
 			}
 		},
+		"node_modules/@types/probe-image-size": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.2.0.tgz",
+			"integrity": "sha512-R5H3vw62gHNHrn+JGZbKejb+Z2D/6E5UNVlhCzIaBBLroMQMOFqy5Pap2gM+ZZHdqBtVU0/cx/M6to+mOJcoew==",
+			"dev": true,
+			"dependencies": {
+				"@types/needle": "*",
+				"@types/node": "*"
+			}
+		},
 		"node_modules/@types/qs": {
 			"version": "6.9.7",
 			"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@@ -3892,6 +3913,11 @@
 			"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
 			"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
 		},
+		"node_modules/lodash.merge": {
+			"version": "4.6.2",
+			"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+			"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+		},
 		"node_modules/lodash.once": {
 			"version": "4.1.1",
 			"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -4295,6 +4321,35 @@
 			"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
 			"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
 		},
+		"node_modules/needle": {
+			"version": "2.9.1",
+			"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
+			"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
+			"dependencies": {
+				"debug": "^3.2.6",
+				"iconv-lite": "^0.4.4",
+				"sax": "^1.2.4"
+			},
+			"bin": {
+				"needle": "bin/needle"
+			},
+			"engines": {
+				"node": ">= 4.4.x"
+			}
+		},
+		"node_modules/needle/node_modules/debug": {
+			"version": "3.2.7",
+			"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+			"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+			"dependencies": {
+				"ms": "^2.1.1"
+			}
+		},
+		"node_modules/needle/node_modules/ms": {
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+			"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+		},
 		"node_modules/negotiator": {
 			"version": "0.6.3",
 			"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -4731,6 +4786,16 @@
 				"url": "https://github.com/prettier/prettier?sponsor=1"
 			}
 		},
+		"node_modules/probe-image-size": {
+			"version": "7.2.3",
+			"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
+			"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
+			"dependencies": {
+				"lodash.merge": "^4.6.2",
+				"needle": "^2.5.2",
+				"stream-parser": "~0.3.1"
+			}
+		},
 		"node_modules/process-nextick-args": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -5341,6 +5406,14 @@
 				"node": ">= 0.8"
 			}
 		},
+		"node_modules/stream-parser": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
+			"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
+			"dependencies": {
+				"debug": "2"
+			}
+		},
 		"node_modules/streamsearch": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -7541,6 +7614,15 @@
 				"@types/express": "*"
 			}
 		},
+		"@types/needle": {
+			"version": "2.5.3",
+			"resolved": "https://registry.npmjs.org/@types/needle/-/needle-2.5.3.tgz",
+			"integrity": "sha512-RwgTwMRaedfyCBe5SSWMpm1Yqzc5UPZEMw0eAd09OSyV93nLRj9/evMGZmgFeHKzUOd4xxtHvgtc+rjcBjI1Qg==",
+			"dev": true,
+			"requires": {
+				"@types/node": "*"
+			}
+		},
 		"@types/node": {
 			"version": "18.7.20",
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.20.tgz",
@@ -7583,6 +7665,16 @@
 				"@types/node": "*"
 			}
 		},
+		"@types/probe-image-size": {
+			"version": "7.2.0",
+			"resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.2.0.tgz",
+			"integrity": "sha512-R5H3vw62gHNHrn+JGZbKejb+Z2D/6E5UNVlhCzIaBBLroMQMOFqy5Pap2gM+ZZHdqBtVU0/cx/M6to+mOJcoew==",
+			"dev": true,
+			"requires": {
+				"@types/needle": "*",
+				"@types/node": "*"
+			}
+		},
 		"@types/qs": {
 			"version": "6.9.7",
 			"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@@ -9221,6 +9313,11 @@
 			"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
 			"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
 		},
+		"lodash.merge": {
+			"version": "4.6.2",
+			"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+			"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+		},
 		"lodash.once": {
 			"version": "4.1.1",
 			"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@@ -9542,6 +9639,31 @@
 			"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
 			"integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg=="
 		},
+		"needle": {
+			"version": "2.9.1",
+			"resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz",
+			"integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==",
+			"requires": {
+				"debug": "^3.2.6",
+				"iconv-lite": "^0.4.4",
+				"sax": "^1.2.4"
+			},
+			"dependencies": {
+				"debug": {
+					"version": "3.2.7",
+					"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+					"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+					"requires": {
+						"ms": "^2.1.1"
+					}
+				},
+				"ms": {
+					"version": "2.1.3",
+					"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+					"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+				}
+			}
+		},
 		"negotiator": {
 			"version": "0.6.3",
 			"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -9868,6 +9990,16 @@
 			"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
 			"dev": true
 		},
+		"probe-image-size": {
+			"version": "7.2.3",
+			"resolved": "https://registry.npmjs.org/probe-image-size/-/probe-image-size-7.2.3.tgz",
+			"integrity": "sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==",
+			"requires": {
+				"lodash.merge": "^4.6.2",
+				"needle": "^2.5.2",
+				"stream-parser": "~0.3.1"
+			}
+		},
 		"process-nextick-args": {
 			"version": "2.0.1",
 			"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -10325,6 +10457,14 @@
 			"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 			"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
 		},
+		"stream-parser": {
+			"version": "0.3.1",
+			"resolved": "https://registry.npmjs.org/stream-parser/-/stream-parser-0.3.1.tgz",
+			"integrity": "sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==",
+			"requires": {
+				"debug": "2"
+			}
+		},
 		"streamsearch": {
 			"version": "1.1.0",
 			"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
diff --git a/package.json b/package.json
index 9c86f918..303324f5 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
 		"@types/node": "^18.7.20",
 		"@types/node-fetch": "^2.6.2",
 		"@types/node-os-utils": "^1.3.0",
+		"@types/probe-image-size": "^7.2.0",
 		"@types/sharp": "^0.31.0",
 		"@types/ws": "^8.5.3",
 		"express": "^4.18.1",
@@ -72,6 +73,7 @@
 		"node-fetch": "^2.6.7",
 		"node-os-utils": "^1.3.7",
 		"picocolors": "^1.0.0",
+		"probe-image-size": "^7.2.3",
 		"proxy-agent": "^5.0.0",
 		"sharp": "^0.31.0",
 		"sqlite3": "^5.1.1",
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