diff --git a/src/plugins/crashHandler.ts b/src/plugins/crashHandler.ts index 61b06b35..c70d467f 100644 --- a/src/plugins/crashHandler.ts +++ b/src/plugins/crashHandler.ts @@ -43,6 +43,7 @@ const settings = definePluginSettings({ let crashCount: number = 0; let lastCrashTimestamp: number = 0; +let shouldAttemptNextHandle = false; export default definePlugin({ name: "CrashHandler", @@ -72,6 +73,10 @@ export default definePlugin({ ], handleCrash(_this: ReactElement & { forceUpdate: () => void; }) { + if (Date.now() - lastCrashTimestamp <= 1_000 && !shouldAttemptNextHandle) return true; + + shouldAttemptNextHandle = false; + if (++crashCount > 5) { try { showNotification({ @@ -151,6 +156,7 @@ export default definePlugin({ } try { + shouldAttemptNextHandle = true; _this.forceUpdate(); } catch (err) { CrashHandlerLogger.debug("Failed to update crash handler component.", err); diff --git a/src/plugins/fakeNitro.tsx b/src/plugins/fakeNitro.tsx index e703af56..0a42ab1d 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; + 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,37 +417,146 @@ export default definePlugin({ }); }, - EmojiComponent: null as any, - - patchFakeNitroEmojis(content: Array, inline: boolean) { - if (!this.EmojiComponent) return content; + patchFakeNitroEmojisOrRemoveStickersLinks(content: Array, inline: boolean) { + if (content.length > 1) return content; const newContent: Array = []; + 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); + 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; + } + } + + 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, message: Message) { + const itemsToMaybePush: Array = []; + + 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; } - newContent.push(( - - )); + 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 newContent; + 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) { @@ -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; - 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)}`; }); diff --git a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx index d2adbb00..90500371 100644 --- a/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx +++ b/src/plugins/showHiddenChannels/components/HiddenChannelLockScreen.tsx @@ -77,13 +77,8 @@ enum ChannelFlags { REQUIRE_TAG = 1 << 4 } -let EmojiComponent: ComponentType; let ChannelBeginHeader: ComponentType; -export function setEmojiComponent(component: ComponentType) { - EmojiComponent = component; -} - export function setChannelBeginHeaderComponent(component: ComponentType) { ChannelBeginHeader = component; } @@ -245,11 +240,10 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) { {defaultReactionEmoji != null &&
Default reaction emoji: - + })}
} {channel.hasFlag(ChannelFlags.REQUIRE_TAG) && diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index e0c12455..d69085da 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -27,7 +27,7 @@ import { findByPropsLazy } from "@webpack"; import { ChannelStore, PermissionStore, Tooltip } from "@webpack/common"; import { Channel } from "discord-types/general"; -import HiddenChannelLockScreen, { setChannelBeginHeaderComponent, setEmojiComponent } from "./components/HiddenChannelLockScreen"; +import HiddenChannelLockScreen, { setChannelBeginHeaderComponent } from "./components/HiddenChannelLockScreen"; const ChannelListClasses = findByPropsLazy("channelName", "subtitle", "modeMuted", "iconContainer"); @@ -234,14 +234,6 @@ export default definePlugin({ replace: ".filter(ch=>!$self.isHiddenChannel(ch))" } }, - // Export the emoji component used on the lock screen - { - find: 'jumboable?"jumbo":"default"', - replacement: { - match: /jumboable\?"jumbo":"default",emojiId.+?}}\)},(?<=(\i)=function\(\i\){var \i=\i\.node.+?)/, - replace: (m, component) => `${m}shcEmojiComponentExport=($self.setEmojiComponent(${component}),void 0),` - } - }, { find: ".Messages.ROLE_REQUIRED_SINGLE_USER_MESSAGE", replacement: [ @@ -403,7 +395,6 @@ export default definePlugin({ } ], - setEmojiComponent, setChannelBeginHeaderComponent, isHiddenChannel(channel: Channel & { channelId?: string; }, checkConnect = false) {