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] 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 }); }