From 4dfaa879617f0867fce76d1034d3dbbd606da1c2 Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Thu, 9 May 2024 23:36:10 +0200
Subject: [PATCH 1/8] initial commit
---
src/plugins/svgEmbed/README.md | 3 +
src/plugins/svgEmbed/index.ts | 106 +++++++++++++++++++++++++++++++++
2 files changed, 109 insertions(+)
create mode 100644 src/plugins/svgEmbed/README.md
create mode 100644 src/plugins/svgEmbed/index.ts
diff --git a/src/plugins/svgEmbed/README.md b/src/plugins/svgEmbed/README.md
new file mode 100644
index 000000000..0042407a9
--- /dev/null
+++ b/src/plugins/svgEmbed/README.md
@@ -0,0 +1,3 @@
+# SVGEmbed
+
+Makes SVG files which are uploaded directly to Discord embed like normal images.
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
new file mode 100644
index 000000000..c4a8ab253
--- /dev/null
+++ b/src/plugins/svgEmbed/index.ts
@@ -0,0 +1,106 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+import { FluxDispatcher } from "@webpack/common";
+import Message from "discord-types/general/Message";
+
+const MinSVGWidth = 400;
+const MinSVGHeight = 350;
+// Limit the size to prevent lag when parsing big files
+const MaxSVGSizeMB = 10;
+
+async function getSVGDimensions(svgUrl: string) {
+ let width = 0, height = 0;
+
+ const res = await fetch(svgUrl);
+ const svgData = await res.text();
+
+ const svgElement = new DOMParser().parseFromString(svgData, "image/svg+xml").documentElement as unknown as SVGSVGElement;
+
+ // Return 0,0 on error, so that the renderer falls back to displaying the raw content
+ const errorNode = svgElement.querySelector("parsererror");
+ if (errorNode) {
+ return { width, height };
+ }
+
+ if (svgElement.width && svgElement.height && svgElement.width.baseVal.unitType === 1 && svgElement.height.baseVal.unitType === 1) {
+ width = svgElement.width.baseVal.value;
+ height = svgElement.height.baseVal.value;
+ } else {
+ width = svgElement.viewBox.baseVal.width;
+ height = svgElement.viewBox.baseVal.height;
+ }
+
+ // If the dimensions are below the minimum values,
+ // scale them up by the smallest integer which makes at least 1 of them exceed it
+ if (width < MinSVGWidth && height < MinSVGHeight) {
+ const multiplier = Math.ceil(Math.min(MinSVGWidth / width, MinSVGHeight / height));
+ width *= multiplier;
+ height *= multiplier;
+ }
+
+ return { width, height };
+}
+
+export default definePlugin({
+ name: "SVGEmbed",
+ description: "Makes SVG files embed as images.",
+ authors: [Devs.amia, Devs.nakoyasha],
+
+ patches: [
+ {
+ find: "isImageFile:function()",
+ replacement: {
+ match: /(?<=png\|jpe\?g\|webp\|gif\|)/,
+ replace: "svg|"
+ }
+ },
+ {
+ find: ".Messages.REMOVE_ATTACHMENT_BODY",
+ replacement: {
+ match: /(?<=renderAttachments\(\i\){)/,
+ replace: "$self.processAttachments(arguments[0]);"
+ }
+ }
+ ],
+
+ async processAttachments(message: Message) {
+ let updateMessage = false;
+
+ const toProcess = message.attachments.filter(x => x.content_type?.startsWith("image/svg+xml") && x.width == null && x.height == null);
+ for (const attachment of toProcess) {
+ if (attachment.size / 1024 / 1024 > MaxSVGSizeMB) continue;
+
+ const { width, height } = await getSVGDimensions(attachment.url);
+ attachment.width = width;
+ attachment.height = height;
+
+ // Change the media.discordapp.net url to use cdn.discordapp.com
+ // since the media one will return http 415 for svgs
+ attachment.proxy_url = attachment.url;
+
+ updateMessage = true;
+ }
+
+ if (updateMessage) {
+ FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
+ }
+ }
+});
From 332e434a74abe2aa3318e26148a4349d551bea05 Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Fri, 10 May 2024 02:21:49 +0200
Subject: [PATCH 2/8] add link embeds support
---
src/plugins/svgEmbed/README.md | 2 +-
src/plugins/svgEmbed/index.ts | 118 +++++++++++++++++++++++++++++----
2 files changed, 106 insertions(+), 14 deletions(-)
diff --git a/src/plugins/svgEmbed/README.md b/src/plugins/svgEmbed/README.md
index 0042407a9..f790dc0ad 100644
--- a/src/plugins/svgEmbed/README.md
+++ b/src/plugins/svgEmbed/README.md
@@ -1,3 +1,3 @@
# SVGEmbed
-Makes SVG files which are uploaded directly to Discord embed like normal images.
+Makes SVG files which are uploaded directly to Discord or linked via `discord.com`/`cdn.discordapp.com` embed like normal images.
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index c4a8ab253..65bc53262 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -16,21 +16,55 @@
* along with this program. If not, see .
*/
+import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
+import definePlugin, { OptionType } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import Message from "discord-types/general/Message";
-const MinSVGWidth = 400;
-const MinSVGHeight = 350;
+const EMBED_SUPPRESSED = 1 << 2;
+
+const MAX_EMBEDS_PER_MESSAGE = 5;
+const MIN_SVG_WIDTH = 400;
+const MIN_SVG_HEIGHT = 350;
// Limit the size to prevent lag when parsing big files
-const MaxSVGSizeMB = 10;
+const MAX_SVG_SIZE_MB = 10;
+
+const URL_REGEX = new RegExp(
+ /(?)/g,
+);
+
+// Cache to avoid excessive requests in component updates
+const FileSizeCache: Map = new Map();
+async function getFileSize(url: string) {
+ if (FileSizeCache.has(url)) {
+ return FileSizeCache.get(url);
+ }
+
+ let size = 0;
+ try {
+ const res = await fetch(url, { method: "HEAD" });
+ const contentLength = res.headers.get("Content-Length");
+
+ if (contentLength) {
+ size = parseInt(contentLength);
+ }
+ } catch (ex) { }
+
+ FileSizeCache.set(url, size);
+ return size;
+}
async function getSVGDimensions(svgUrl: string) {
let width = 0, height = 0;
+ let svgData: string;
- const res = await fetch(svgUrl);
- const svgData = await res.text();
+ try {
+ const res = await fetch(svgUrl);
+ svgData = await res.text();
+ } catch (ex) {
+ return { width, height };
+ }
const svgElement = new DOMParser().parseFromString(svgData, "image/svg+xml").documentElement as unknown as SVGSVGElement;
@@ -50,8 +84,8 @@ async function getSVGDimensions(svgUrl: string) {
// If the dimensions are below the minimum values,
// scale them up by the smallest integer which makes at least 1 of them exceed it
- if (width < MinSVGWidth && height < MinSVGHeight) {
- const multiplier = Math.ceil(Math.min(MinSVGWidth / width, MinSVGHeight / height));
+ if (width < MIN_SVG_WIDTH && height < MIN_SVG_HEIGHT) {
+ const multiplier = Math.ceil(Math.min(MIN_SVG_WIDTH / width, MIN_SVG_HEIGHT / height));
width *= multiplier;
height *= multiplier;
}
@@ -59,10 +93,20 @@ async function getSVGDimensions(svgUrl: string) {
return { width, height };
}
+const settings = definePluginSettings({
+ embedLinks: {
+ type: OptionType.BOOLEAN,
+ description: "Embed SVG links hosted under discord.com and cdn.discordapp.com",
+ default: true,
+ restartNeeded: true
+ },
+});
+
export default definePlugin({
name: "SVGEmbed",
description: "Makes SVG files embed as images.",
authors: [Devs.amia, Devs.nakoyasha],
+ settings: settings,
patches: [
{
@@ -74,10 +118,17 @@ export default definePlugin({
},
{
find: ".Messages.REMOVE_ATTACHMENT_BODY",
- replacement: {
- match: /(?<=renderAttachments\(\i\){)/,
- replace: "$self.processAttachments(arguments[0]);"
- }
+ replacement: [
+ {
+ match: /(?<=renderAttachments\(\i\){)/,
+ replace: "$self.processAttachments(arguments[0]);"
+ },
+ {
+ match: /(?<=renderEmbeds\(\i\){)/,
+ predicate: () => settings.store.embedLinks,
+ replace: "$self.processEmbeds(arguments[0]);"
+ }
+ ]
}
],
@@ -86,7 +137,7 @@ export default definePlugin({
const toProcess = message.attachments.filter(x => x.content_type?.startsWith("image/svg+xml") && x.width == null && x.height == null);
for (const attachment of toProcess) {
- if (attachment.size / 1024 / 1024 > MaxSVGSizeMB) continue;
+ if (attachment.size / 1024 / 1024 > MAX_SVG_SIZE_MB) continue;
const { width, height } = await getSVGDimensions(attachment.url);
attachment.width = width;
@@ -99,6 +150,47 @@ export default definePlugin({
updateMessage = true;
}
+ if (updateMessage) {
+ FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
+ }
+ },
+
+ async processEmbeds(message: Message) {
+ if (message.hasFlag(EMBED_SUPPRESSED)) return;
+
+ let updateMessage = false;
+
+ const svgUrls = new Set(message.content.match(URL_REGEX));
+ const existingUrls = new Set(message.embeds.filter(x => x.type === "image").map(x => x.image?.url));
+
+ let imageEmbedsCount = existingUrls.size;
+ for (const url of [...svgUrls.values()]) {
+ if (imageEmbedsCount >= MAX_EMBEDS_PER_MESSAGE) break;
+ if (existingUrls.has(url)) continue;
+
+ // Check size of files on the cdn.
+ // The files under https://discord.com/assets/* don't return Content-Length
+ // but they don't really have to be checked.
+ const domain = new URL(url).hostname;
+ if (domain === "cdn.discordapp.com") {
+ const size = await getFileSize(url);
+ if (!size || size / 1024 / 1024 > MAX_SVG_SIZE_MB) continue;
+ }
+
+ const { width, height } = await getSVGDimensions(url);
+ // @ts-ignore
+ message.embeds.push({
+ id: "embed_1",
+ url,
+ type: "image",
+ image: { url, proxyURL: url, width, height },
+ fields: []
+ });
+
+ imageEmbedsCount++;
+ updateMessage = true;
+ }
+
if (updateMessage) {
FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
}
From d96ff463f5d06a5955c9dcbb421843348f77a6bd Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Fri, 24 May 2024 17:14:44 +0200
Subject: [PATCH 3/8] clarify embed id
---
src/plugins/svgEmbed/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index 65bc53262..12c6beda0 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -180,7 +180,7 @@ export default definePlugin({
const { width, height } = await getSVGDimensions(url);
// @ts-ignore
message.embeds.push({
- id: "embed_1",
+ id: "embed_1", // The id can be anything as it seems to be changed by the client anyways
url,
type: "image",
image: { url, proxyURL: url, width, height },
From 7c62039a096cde89173669a0c6678ddb5dd199a6 Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Fri, 24 May 2024 17:15:53 +0200
Subject: [PATCH 4/8] switch from Content-Length to Range
---
src/plugins/svgEmbed/index.ts | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index 12c6beda0..ff2e02c59 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -35,24 +35,30 @@ const URL_REGEX = new RegExp(
);
// Cache to avoid excessive requests in component updates
-const FileSizeCache: Map = new Map();
-async function getFileSize(url: string) {
+const FileSizeCache: Map = new Map();
+async function checkFileSize(url: string) {
if (FileSizeCache.has(url)) {
return FileSizeCache.get(url);
}
- let size = 0;
+ let belowMaxSize = false;
try {
- const res = await fetch(url, { method: "HEAD" });
- const contentLength = res.headers.get("Content-Length");
+ // We cannot check Content-Length due to Access-Control-Expose-Headers.
+ // Instead, we request 1 byte past the size limit, and see if it succeeds.
+ const res = await fetch(url, {
+ method: "HEAD",
+ headers: {
+ Range: `bytes=${MAX_SVG_SIZE_MB * 1024 * 1024}-${MAX_SVG_SIZE_MB * 1024 * 1024 + 1}`
+ }
+ });
- if (contentLength) {
- size = parseInt(contentLength);
+ if (res.status === 416) {
+ belowMaxSize = true;
}
} catch (ex) { }
- FileSizeCache.set(url, size);
- return size;
+ FileSizeCache.set(url, belowMaxSize);
+ return belowMaxSize;
}
async function getSVGDimensions(svgUrl: string) {
@@ -168,14 +174,8 @@ export default definePlugin({
if (imageEmbedsCount >= MAX_EMBEDS_PER_MESSAGE) break;
if (existingUrls.has(url)) continue;
- // Check size of files on the cdn.
- // The files under https://discord.com/assets/* don't return Content-Length
- // but they don't really have to be checked.
- const domain = new URL(url).hostname;
- if (domain === "cdn.discordapp.com") {
- const size = await getFileSize(url);
- if (!size || size / 1024 / 1024 > MAX_SVG_SIZE_MB) continue;
- }
+ const belowMaxSize = await checkFileSize(url);
+ if (!belowMaxSize) continue;
const { width, height } = await getSVGDimensions(url);
// @ts-ignore
From a24967ed2fae9caa9831060a98dab30471ab1adb Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Fri, 24 May 2024 17:25:04 +0200
Subject: [PATCH 5/8] don't embed svgs in pending messages
---
src/plugins/svgEmbed/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index ff2e02c59..58ab386cf 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -162,6 +162,7 @@ export default definePlugin({
},
async processEmbeds(message: Message) {
+ if (message.state !== "SENT") return;
if (message.hasFlag(EMBED_SUPPRESSED)) return;
let updateMessage = false;
From 5f5e9c9558185786b471f3abe468ad3b6680a129 Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Fri, 24 May 2024 17:25:25 +0200
Subject: [PATCH 6/8] add debounce
---
src/plugins/svgEmbed/index.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index 58ab386cf..a6fa579a8 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -161,10 +161,14 @@ export default definePlugin({
}
},
+ debounce: new Set(),
async processEmbeds(message: Message) {
if (message.state !== "SENT") return;
if (message.hasFlag(EMBED_SUPPRESSED)) return;
+ if (this.debounce.has(message.id)) return;
+ this.debounce.add(message.id);
+
let updateMessage = false;
const svgUrls = new Set(message.content.match(URL_REGEX));
@@ -195,5 +199,7 @@ export default definePlugin({
if (updateMessage) {
FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
}
+
+ this.debounce.delete(message.id);
}
});
From ad03ee3fb5327f7ab6b68f6702cbc34cf6170489 Mon Sep 17 00:00:00 2001
From: Vendicated
Date: Tue, 28 May 2024 03:25:11 +0200
Subject: [PATCH 7/8] improve naming
---
src/plugins/svgEmbed/index.ts | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index a6fa579a8..9546bc500 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -52,7 +52,7 @@ async function checkFileSize(url: string) {
}
});
- if (res.status === 416) {
+ if (res.status === 416 /* Range Not Satisfiable */) {
belowMaxSize = true;
}
} catch (ex) { }
@@ -139,7 +139,7 @@ export default definePlugin({
],
async processAttachments(message: Message) {
- let updateMessage = false;
+ let shouldUpdateMessage = false;
const toProcess = message.attachments.filter(x => x.content_type?.startsWith("image/svg+xml") && x.width == null && x.height == null);
for (const attachment of toProcess) {
@@ -153,26 +153,26 @@ export default definePlugin({
// since the media one will return http 415 for svgs
attachment.proxy_url = attachment.url;
- updateMessage = true;
+ shouldUpdateMessage = true;
}
- if (updateMessage) {
+ if (shouldUpdateMessage) {
FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
}
},
- debounce: new Set(),
+ currentlyProcessing: new Set(),
async processEmbeds(message: Message) {
if (message.state !== "SENT") return;
if (message.hasFlag(EMBED_SUPPRESSED)) return;
- if (this.debounce.has(message.id)) return;
- this.debounce.add(message.id);
+ if (this.currentlyProcessing.has(message.id)) return;
+ this.currentlyProcessing.add(message.id);
- let updateMessage = false;
+ let shouldUpdateMessage = false;
const svgUrls = new Set(message.content.match(URL_REGEX));
- const existingUrls = new Set(message.embeds.filter(x => x.type === "image").map(x => x.image?.url));
+ const existingUrls = new Set(message.embeds.filter(e => e.type === "image").map(e => e.image?.url));
let imageEmbedsCount = existingUrls.size;
for (const url of [...svgUrls.values()]) {
@@ -183,7 +183,7 @@ export default definePlugin({
if (!belowMaxSize) continue;
const { width, height } = await getSVGDimensions(url);
- // @ts-ignore
+ // @ts-ignore ~ bad types
message.embeds.push({
id: "embed_1", // The id can be anything as it seems to be changed by the client anyways
url,
@@ -193,13 +193,13 @@ export default definePlugin({
});
imageEmbedsCount++;
- updateMessage = true;
+ shouldUpdateMessage = true;
}
- if (updateMessage) {
+ if (shouldUpdateMessage) {
FluxDispatcher.dispatch({ type: "MESSAGE_UPDATE", message });
}
- this.debounce.delete(message.id);
+ this.currentlyProcessing.delete(message.id);
}
});
From b542ee48a74c800b12ca8caa092e9bb7c442924f Mon Sep 17 00:00:00 2001
From: Amia <9750071+aamiaa@users.noreply.github.com>
Date: Thu, 20 Jun 2024 00:30:43 +0200
Subject: [PATCH 8/8] fix patch find
---
src/plugins/svgEmbed/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/plugins/svgEmbed/index.ts b/src/plugins/svgEmbed/index.ts
index 9546bc500..9e07da6ea 100644
--- a/src/plugins/svgEmbed/index.ts
+++ b/src/plugins/svgEmbed/index.ts
@@ -116,7 +116,7 @@ export default definePlugin({
patches: [
{
- find: "isImageFile:function()",
+ find: "png|jpe?g|webp|gif|",
replacement: {
match: /(?<=png\|jpe\?g\|webp\|gif\|)/,
replace: "svg|"