Fake Nitro Transform Stickers option and other stuff (#683)

Co-authored-by: V <vendicated@riseup.net>
This commit is contained in:
Nuckyz 2023-04-05 00:06:04 -03:00 committed by GitHub
parent 99391a4f0e
commit 12ffb9d642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 269 additions and 76 deletions

View File

@ -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);

View File

@ -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,37 +417,146 @@ 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);
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<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;
}
newContent.push((
<this.EmojiComponent node={{
type: "customEmoji",
jumboable: !inline && content.length === 1,
animated: fakeNitroMatch[2] === "gif",
name: ":FakeNitroEmoji:",
emojiId: fakeNitroMatch[1]
}} />
));
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<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)}`;
});

View File

@ -77,13 +77,8 @@ enum ChannelFlags {
REQUIRE_TAG = 1 << 4
}
let EmojiComponent: ComponentType<any>;
let ChannelBeginHeader: ComponentType<any>;
export function setEmojiComponent(component: ComponentType<any>) {
EmojiComponent = component;
}
export function setChannelBeginHeaderComponent(component: ComponentType<any>) {
ChannelBeginHeader = component;
}
@ -245,11 +240,10 @@ function HiddenChannelLockScreen({ channel }: { channel: ExtendedChannel; }) {
{defaultReactionEmoji != null &&
<div className="shc-lock-screen-default-emoji-container">
<Text variant="text-md/normal">Default reaction emoji:</Text>
<EmojiComponent node={{
type: defaultReactionEmoji.emojiName ? "emoji" : "customEmoji",
{Parser.defaultRules[defaultReactionEmoji.emojiName ? "emoji" : "customEmoji"].react({
name: defaultReactionEmoji.emojiName ?? "",
emojiId: defaultReactionEmoji.emojiId
}} />
})}
</div>
}
{channel.hasFlag(ChannelFlags.REQUIRE_TAG) &&

View File

@ -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) {