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|"