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
|