From 12ffb9d64276dd36c7e1fd7c184e60448fbf748f Mon Sep 17 00:00:00 2001
From: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Date: Wed, 5 Apr 2023 00:06:04 -0300
Subject: Fake Nitro Transform Stickers option and other stuff (#683)

Co-authored-by: V <vendicated@riseup.net>
---
 src/plugins/fakeNitro.tsx | 318 +++++++++++++++++++++++++++++++++++++---------
 1 file changed, 260 insertions(+), 58 deletions(-)

(limited to 'src/plugins/fakeNitro.tsx')

diff --git a/src/plugins/fakeNitro.tsx b/src/plugins/fakeNitro.tsx
index e703af5..0a42ab1 100644
--- a/src/plugins/fakeNitro.tsx
+++ b/src/plugins/fakeNitro.tsx
@@ -17,20 +17,28 @@
 */
 
 import { addPreEditListener, addPreSendListener, removePreEditListener, removePreSendListener } from "@api/MessageEvents";
-import { migratePluginSettings, Settings } from "@api/settings";
+import { definePluginSettings, migratePluginSettings, Settings } from "@api/settings";
 import { Devs } from "@utils/constants";
-import { ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
+import { ApngBlendOp, ApngDisposeOp, getGifEncoder, importApngJs } from "@utils/dependencies";
 import { getCurrentGuild } from "@utils/discord";
 import { proxyLazy } from "@utils/proxyLazy";
 import definePlugin, { OptionType } from "@utils/types";
 import { findByCodeLazy, findByPropsLazy, findLazy, findStoreLazy } from "@webpack";
-import { ChannelStore, FluxDispatcher, PermissionStore, UserStore } from "@webpack/common";
+import { ChannelStore, FluxDispatcher, Parser, PermissionStore, UserStore } from "@webpack/common";
+import type { Message } from "discord-types/general";
 
 const DRAFT_TYPE = 0;
 const promptToUpload = findByCodeLazy("UPLOAD_FILE_LIMIT_ERROR");
 const UserSettingsProtoStore = findStoreLazy("UserSettingsProtoStore");
 const PreloadedUserSettingsProtoHandler = findLazy(m => m.ProtoClass?.typeName === "discord_protos.discord_users.v1.PreloadedUserSettings");
 const ReaderFactory = findByPropsLazy("readerFactory");
+const StickerStore = findStoreLazy("StickersStore") as {
+    getPremiumPacks(): StickerPack[];
+    getAllGuildStickers(): Map<string, Sticker[]>;
+    getStickerById(id: string): Sticker | undefined;
+};
+const EmojiStore = findStoreLazy("EmojiStore");
+
 
 function searchProtoClass(localName: string, parentProtoClass: any) {
     if (!parentProtoClass) return;
@@ -86,6 +94,55 @@ interface StickerPack {
     stickers: Sticker[];
 }
 
+const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;
+const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;
+const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;
+
+const settings = definePluginSettings({
+    enableEmojiBypass: {
+        description: "Allow sending fake emojis",
+        type: OptionType.BOOLEAN,
+        default: true,
+        restartNeeded: true
+    },
+    emojiSize: {
+        description: "Size of the emojis when sending",
+        type: OptionType.SLIDER,
+        default: 48,
+        markers: [32, 48, 64, 128, 160, 256, 512]
+    },
+    transformEmojis: {
+        description: "Whether to transform fake emojis into real ones",
+        type: OptionType.BOOLEAN,
+        default: true,
+        restartNeeded: true
+    },
+    enableStickerBypass: {
+        description: "Allow sending fake stickers",
+        type: OptionType.BOOLEAN,
+        default: true,
+        restartNeeded: true
+    },
+    stickerSize: {
+        description: "Size of the stickers when sending",
+        type: OptionType.SLIDER,
+        default: 160,
+        markers: [32, 64, 128, 160, 256, 512]
+    },
+    transformStickers: {
+        description: "Whether to transform fake stickers into real ones",
+        type: OptionType.BOOLEAN,
+        default: true,
+        restartNeeded: true
+    },
+    enableStreamQualityBypass: {
+        description: "Allow streaming in nitro quality",
+        type: OptionType.BOOLEAN,
+        default: true,
+        restartNeeded: true
+    }
+});
+
 migratePluginSettings("FakeNitro", "NitroBypass");
 
 export default definePlugin({
@@ -94,10 +151,12 @@ export default definePlugin({
     description: "Allows you to stream in nitro quality, send fake emojis/stickers and use client themes.",
     dependencies: ["MessageEventsAPI"],
 
+    settings,
+
     patches: [
         {
             find: ".PREMIUM_LOCKED;",
-            predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
+            predicate: () => settings.store.enableEmojiBypass,
             replacement: [
                 {
                     match: /(?<=(\i)=\i\.intention)/,
@@ -115,7 +174,7 @@ export default definePlugin({
         },
         {
             find: "canUseAnimatedEmojis:function",
-            predicate: () => Settings.plugins.FakeNitro.enableEmojiBypass === true,
+            predicate: () => settings.store.enableEmojiBypass,
             replacement: {
                 match: /((?:canUseEmojisEverywhere|canUseAnimatedEmojis):function\(\i)\){(.+?\))/g,
                 replace: (_, rest, premiumCheck) => `${rest},fakeNitroIntention){${premiumCheck}||fakeNitroIntention==null||[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`
@@ -123,7 +182,7 @@ export default definePlugin({
         },
         {
             find: "canUseStickersEverywhere:function",
-            predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
+            predicate: () => settings.store.enableStickerBypass,
             replacement: {
                 match: /canUseStickersEverywhere:function\(\i\){/,
                 replace: "$&return true;"
@@ -131,7 +190,7 @@ export default definePlugin({
         },
         {
             find: "\"SENDABLE\"",
-            predicate: () => Settings.plugins.FakeNitro.enableStickerBypass === true,
+            predicate: () => settings.store.enableStickerBypass,
             replacement: {
                 match: /(\w+)\.available\?/,
                 replace: "true?"
@@ -139,7 +198,7 @@ export default definePlugin({
         },
         {
             find: "canStreamHighQuality:function",
-            predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
+            predicate: () => settings.store.enableStreamQualityBypass,
             replacement: [
                 "canUseHighVideoUploadQuality",
                 "canStreamHighQuality",
@@ -153,7 +212,7 @@ export default definePlugin({
         },
         {
             find: "STREAM_FPS_OPTION.format",
-            predicate: () => Settings.plugins.FakeNitro.enableStreamQualityBypass === true,
+            predicate: () => settings.store.enableStreamQualityBypass,
             replacement: {
                 match: /(userPremiumType|guildPremiumTier):.{0,10}TIER_\d,?/g,
                 replace: ""
@@ -186,34 +245,61 @@ export default definePlugin({
                 replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`
             }
         },
-        {
-            find: 'jumboable?"jumbo":"default"',
-            predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
-            replacement: {
-                match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/,
-                replace: (m, component) => `${m}fakeNitroEmojiComponentExport=($self.EmojiComponent=${component},void 0),`
-            }
-        },
         {
             find: '["strong","em","u","text","inlineCode","s","spoiler"]',
-            predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
             replacement: [
                 {
+                    predicate: () => settings.store.transformEmojis,
                     match: /1!==(\i)\.length\|\|1!==\i\.length/,
-                    replace: (m, content) => `${m}||${content}[0].target?.startsWith("https://cdn.discordapp.com/emojis/")`
+                    replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
                 },
                 {
+                    predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
                     match: /(?=return{hasSpoilerEmbeds:\i,content:(\i)})/,
-                    replace: (_, content) => `${content}=$self.patchFakeNitroEmojis(${content},arguments[2]?.formatInline);`
+                    replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
                 }
             ]
         },
         {
             find: "renderEmbeds=function",
-            predicate: () => Settings.plugins.FakeNitro.transformEmojis === true,
+            replacement: [
+                {
+                    predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
+                    match: /(renderEmbeds=function\((\i)\){)(.+?embeds\.map\(\(function\((\i)\){)/,
+                    replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`
+                },
+                {
+                    predicate: () => settings.store.transformStickers,
+                    match: /renderStickersAccessories=function\((\i)\){var (\i)=\(0,\i\.\i\)\(\i\),/,
+                    replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message}),`
+                },
+                {
+                    predicate: () => settings.store.transformStickers,
+                    match: /renderAttachments=function\(\i\){var (\i)=\i.attachments.+?;/,
+                    replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`
+                }
+            ]
+        },
+        {
+            find: ".STICKER_IN_MESSAGE_HOVER,",
+            predicate: () => settings.store.transformStickers,
+            replacement: [
+                {
+                    match: /var (\i)=\i\.renderableSticker,.{0,50}closePopout.+?channel:\i,closePopout:\i,/,
+                    replace: (m, renderableSticker) => `${m}renderableSticker:${renderableSticker},`
+                },
+                {
+                    match: /emojiSection.{0,50}description:\i(?<=(\i)\.sticker,.+?)(?=,)/,
+                    replace: (m, props) => `${m}+(${props}.renderableSticker?.fake?" This is a Fake Nitro sticker. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")`
+                }
+            ]
+        },
+        {
+            find: ".Messages.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION",
+            predicate: () => settings.store.transformEmojis,
             replacement: {
-                match: /renderEmbeds=function\(\i\){.+?embeds\.map\(\(function\((\i)\){/,
-                replace: (m, embed) => `${m}if(${embed}.url?.startsWith("https://cdn.discordapp.com/emojis/"))return null;`
+                match: /((\i)=\i\.node,\i=\i\.emojiSourceDiscoverableGuild)(.+?return) (.{0,450}Messages\.EMOJI_POPOUT_PREMIUM_JOINED_GUILD_DESCRIPTION.+?}\))/,
+                replace: (_, rest1, node, rest2, messages) => `${rest1},fakeNitroNode=${node}${rest2}(${messages})+(fakeNitroNode.fake?" This is a Fake Nitro emoji. Only you can see it rendered like a real one, for non Vencord users it will show as a link.":"")`
             }
         }
     ],
@@ -331,39 +417,148 @@ export default definePlugin({
         });
     },
 
-    EmojiComponent: null as any,
-
-    patchFakeNitroEmojis(content: Array<any>, inline: boolean) {
-        if (!this.EmojiComponent) return content;
+    patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
+        if (content.length > 1) return content;
 
         const newContent: Array<any> = [];
 
+        let nextIndex = content.length;
+
         for (const element of content) {
             if (element.props?.trusted == null) {
                 newContent.push(element);
                 continue;
             }
 
-            const fakeNitroMatch = element.props.href.match(/https:\/\/cdn\.discordapp\.com\/emojis\/(\d+?)\.(png|webp|gif).+?(?=\s|$)/);
-            if (!fakeNitroMatch) {
-                newContent.push(element);
-                continue;
+            if (settings.store.transformEmojis) {
+                const fakeNitroMatch = element.props.href.match(fakeNitroEmojiRegex);
+                if (fakeNitroMatch) {
+                    let url: URL | null = null;
+                    try {
+                        url = new URL(element.props.href);
+                    } catch { }
+
+                    const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
+
+                    newContent.push(Parser.defaultRules.customEmoji.react({
+                        jumboable: !inline,
+                        animated: fakeNitroMatch[2] === "gif",
+                        emojiId: fakeNitroMatch[1],
+                        name: emojiName,
+                        fake: true
+                    }, void 0, { key: String(nextIndex++) }));
+
+                    continue;
+                }
             }
 
-            newContent.push((
-                <this.EmojiComponent node={{
-                    type: "customEmoji",
-                    jumboable: !inline && content.length === 1,
-                    animated: fakeNitroMatch[2] === "gif",
-                    name: ":FakeNitroEmoji:",
-                    emojiId: fakeNitroMatch[1]
-                }} />
-            ));
+            if (settings.store.transformStickers) {
+                if (fakeNitroStickerRegex.test(element.props.href)) continue;
+
+                const gifMatch = element.props.href.match(fakeNitroGifStickerRegex);
+                if (gifMatch) {
+                    // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
+                    if (StickerStore.getStickerById(gifMatch[1])) continue;
+                }
+            }
+
+            newContent.push(element);
         }
 
+        const firstTextElementIdx = newContent.findIndex(element => typeof element === "string");
+        if (firstTextElementIdx !== -1) newContent[firstTextElementIdx] = newContent[firstTextElementIdx].trimStart();
+
         return newContent;
     },
 
+    patchFakeNitroStickers(stickers: Array<any>, message: Message) {
+        const itemsToMaybePush: Array<string> = [];
+
+        const contentItems = message.content.split(/\s/);
+        if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]);
+
+        itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));
+
+        for (const item of itemsToMaybePush) {
+            const imgMatch = item.match(fakeNitroStickerRegex);
+            if (imgMatch) {
+                let url: URL | null = null;
+                try {
+                    url = new URL(item);
+                } catch { }
+
+                const stickerName = StickerStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";
+                stickers.push({
+                    format_type: 1,
+                    id: imgMatch[1],
+                    name: stickerName,
+                    fake: true
+                });
+
+                continue;
+            }
+
+            const gifMatch = item.match(fakeNitroGifStickerRegex);
+            if (gifMatch) {
+                if (!StickerStore.getStickerById(gifMatch[1])) continue;
+
+                const stickerName = StickerStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";
+                stickers.push({
+                    format_type: 2,
+                    id: gifMatch[1],
+                    name: stickerName,
+                    fake: true
+                });
+            }
+        }
+
+        return stickers;
+    },
+
+    shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {
+        if (message.content.split(/\s/).length > 1) return false;
+
+        switch (embed.type) {
+            case "image": {
+                if (settings.store.transformEmojis) {
+                    if (fakeNitroEmojiRegex.test(embed.url!)) return true;
+                }
+
+                if (settings.store.transformStickers) {
+                    if (fakeNitroStickerRegex.test(embed.url!)) return true;
+
+                    const gifMatch = embed.url!.match(fakeNitroGifStickerRegex);
+                    if (gifMatch) {
+                        // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
+                        if (StickerStore.getStickerById(gifMatch[1])) return true;
+                    }
+                }
+
+                break;
+            }
+        }
+
+        return false;
+    },
+
+    filterAttachments(attachments: Message["attachments"]) {
+        return attachments.filter(attachment => {
+            if (attachment.content_type !== "image/gif") return true;
+
+            const match = attachment.url.match(fakeNitroGifStickerRegex);
+            if (match) {
+                // There is no way to differentiate a regular gif attachment from a fake nitro animated sticker, so we check if the StickerStore contains the id of the fake sticker
+                if (StickerStore.getStickerById(match[1])) return false;
+            }
+
+            return true;
+        });
+    },
+
+    shouldKeepEmojiLink(link: any) {
+        return link.target && fakeNitroEmojiRegex.test(link.target);
+    },
+
     hasPermissionToUseExternalEmojis(channelId: string) {
         const channel = ChannelStore.getChannel(channelId);
 
@@ -407,8 +602,17 @@ export default definePlugin({
         const scale = resolution / Math.max(width, height);
         ctx.scale(scale, scale);
 
-        let lastImg: HTMLImageElement | null = null;
-        for (const { left, top, width, height, disposeOp, img, delay } of frames) {
+        let previousFrameData: ImageData;
+
+        for (const frame of frames) {
+            const { left, top, width, height, img, delay, blendOp, disposeOp } = frame;
+
+            previousFrameData = ctx.getImageData(left, top, width, height);
+
+            if (blendOp === ApngBlendOp.SOURCE) {
+                ctx.clearRect(left, top, width, height);
+            }
+
             ctx.drawImage(img, left, top, width, height);
 
             const { data } = ctx.getImageData(0, 0, resolution, resolution);
@@ -419,19 +623,18 @@ export default definePlugin({
             gif.writeFrame(index, resolution, resolution, {
                 transparent: true,
                 palette,
-                delay,
+                delay
             });
 
             if (disposeOp === ApngDisposeOp.BACKGROUND) {
                 ctx.clearRect(left, top, width, height);
-            } else if (disposeOp === ApngDisposeOp.PREVIOUS && lastImg) {
-                ctx.drawImage(lastImg, left, top, width, height);
+            } else if (disposeOp === ApngDisposeOp.PREVIOUS) {
+                ctx.putImageData(previousFrameData, left, top);
             }
-
-            lastImg = img;
         }
 
         gif.finish();
+
         const file = new File([gif.bytesView()], `${stickerId}.gif`, { type: "image/gif" });
         promptToUpload([file], ChannelStore.getChannel(channelId), DRAFT_TYPE);
     },
@@ -442,13 +645,6 @@ export default definePlugin({
             return;
         }
 
-        const EmojiStore = findByPropsLazy("getCustomEmojiById");
-        const StickerStore = findByPropsLazy("getAllGuildStickers") as {
-            getPremiumPacks(): StickerPack[];
-            getAllGuildStickers(): Map<string, Sticker[]>;
-            getStickerById(id: string): Sticker | undefined;
-        };
-
         function getWordBoundary(origStr: string, offset: number) {
             return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";
         }
@@ -469,7 +665,7 @@ export default definePlugin({
 
                 let link = this.getStickerLink(sticker.id);
                 if (sticker.format_type === 2) {
-                    this.sendAnimatedSticker(this.getStickerLink(sticker.id), sticker.id, channelId);
+                    this.sendAnimatedSticker(link, sticker.id, channelId);
                     return { cancel: true };
                 } else {
                     if ("pack_id" in sticker) {
@@ -483,7 +679,7 @@ export default definePlugin({
                     }
 
                     delete extra.stickerIds;
-                    messageObj.content += " " + link;
+                    messageObj.content += " " + link + `&name=${sticker.name}`;
                 }
             }
 
@@ -493,7 +689,10 @@ export default definePlugin({
                     if (emoji.guildId === guildId && !emoji.animated) continue;
 
                     const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;
-                    const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
+                    const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
+                        size: Settings.plugins.FakeNitro.emojiSize,
+                        name: encodeURIComponent(emoji.name)
+                    }));
                     messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {
                         return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
                     });
@@ -513,7 +712,10 @@ export default definePlugin({
                 if (emoji == null || (emoji.guildId === guildId && !emoji.animated)) continue;
                 if (!emoji.require_colons) continue;
 
-                const url = emoji.url.replace(/\?size=\d+/, `?size=${Settings.plugins.FakeNitro.emojiSize}`);
+                const url = emoji.url.replace(/\?size=\d+/, "?" + new URLSearchParams({
+                    size: Settings.plugins.FakeNitro.emojiSize,
+                    name: encodeURIComponent(emoji.name)
+                }));
                 messageObj.content = messageObj.content.replace(emojiStr, (match, offset, origStr) => {
                     return `${getWordBoundary(origStr, offset - 1)}${url}${getWordBoundary(origStr, offset + match.length)}`;
                 });
-- 
cgit 

