fix FakeNitro & ImageLink (#4620)

Co-authored-by: Vendicated <vendicated@riseup.net>
This commit is contained in:
sadan 2026-09-24 20:58:07 -04:00 • committed by GitHub
parent 6c7e9cd9a9
commit de964b6282
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 110 deletions

View file

@ -26,7 +26,7 @@ import definePlugin, { OptionType } from "@utils/types";
import type { Emoji, Message, RenderModalProps, Sticker } from "@vencord/discord-types";
import { StickerFormatType } from "@vencord/discord-types/enums";
import { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from "@webpack";
import { ChannelStore, ConfirmModal, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, openModal, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";
import { ChannelStore, ConfirmModal, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, openModal, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";
import { applyPalette, GIFEncoder, quantize } from "gifenc";
import type { ReactElement, ReactNode } from "react";
@ -40,6 +40,8 @@ function searchProtoClassField(localName: string, protoClass: any) {
return fieldGetter?.();
}
const logger = new Logger("FakeNitro");
const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));
@ -328,20 +330,11 @@ export default definePlugin({
},
{
find: '["strong","em","u","text","inlineCode","s","spoiler"]',
replacement: [
{
// Call our function to decide whether the emoji link should be kept or not
predicate: () => settings.store.transformEmojis,
match: /1!==(\i)\.length\|\|1!==\i\.length/,
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`
},
{
// Patch the rendered message content to add fake nitro emojis or remove sticker links
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
match: /(?=return{hasSpoilerEmbeds:\i,hasBailedAst:\i,content:(\i))/,
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`
}
]
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,
replacement: {
match: /\(\{ast:(\i)(?=,inline:\i)/,
replace: "({ast:$self.transformAst($1)"
}
},
{
find: "}renderStickersAccessories(",
@ -520,107 +513,58 @@ export default definePlugin({
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];
},
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {
// If content has more than one child or it's a single ReactElement like a header, list or span
if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content;
transformAst(ast: any[]) {
try {
if (!settings.store.transformCompoundSentence && ast.length > 1) {
return ast;
}
// Filter out sticker links. These are transformed into actual stickers later, so we don't want duplicate links
if (settings.store.transformStickers) {
ast = ast.filter(node => {
const { type, target } = node;
let nextIndex = content.length;
if (type !== "link") return true;
if (fakeNitroStickerRegex.test(target)) return false;
const gifStickerLinkMatch = target.match(fakeNitroGifStickerRegex);
const isGifStickerLink = gifStickerLinkMatch && StickersStore.getStickerById(gifStickerLinkMatch[1]);
return !isGifStickerLink;
});
}
const transformLinkChild = (child: ReactElement<any>) => {
if (settings.store.transformEmojis) {
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);
if (fakeNitroMatch) {
ast = ast.map(node => {
const { type, target } = node;
if (type !== "link") return node;
const fakeNitroMatch = target.match(fakeNitroEmojiRegex);
if (!fakeNitroMatch) return node;
let url: URL | null = null;
try {
url = new URL(child.props.href);
url = new URL(target);
} catch { }
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
const isAnimated = fakeNitroMatch[2] === "gif" || url?.searchParams.get("animated") === "true";
const emojiId = fakeNitroMatch[1];
const emojiName = EmojiStore.getCustomEmojiById(emojiId)?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";
const animated = fakeNitroMatch[2] === "gif" || url?.searchParams.get("animated") === "true";
return Parser.defaultRules.customEmoji.react({
jumboable: !inline && content.length === 1 && typeof content[0].type !== "string",
animated: isAnimated,
emojiId: fakeNitroMatch[1],
name: emojiName,
return {
animated,
emojiId,
name: `:${emojiName}:`,
type: "customEmoji",
fake: true
}, void 0, { key: String(nextIndex++) });
}
};
});
}
if (settings.store.transformStickers) {
if (fakeNitroStickerRegex.test(child.props.href)) return null;
const gifMatch = child.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 StickersStore contains the id of the fake sticker
if (StickersStore.getStickerById(gifMatch[1])) return null;
}
}
return child;
};
const transformChild = (child: ReactElement<any>) => {
if (child?.props?.trusted != null) return transformLinkChild(child);
if (child?.props?.children != null) {
if (!Array.isArray(child.props.children)) {
child.props.children = modifyChild(child.props.children);
return child;
}
child.props.children = modifyChildren(child.props.children);
if (child.props.children.length === 0) return null;
return child;
}
return child;
};
const modifyChild = (child: ReactElement<any>) => {
const newChild = transformChild(child);
if (newChild?.type === "ul" || newChild?.type === "ol") {
this.ensureChildrenIsArray(newChild);
if (newChild.props.children.length === 0) return null;
let listHasAnItem = false;
for (const [index, child] of newChild.props.children.entries()) {
if (child == null) {
delete newChild.props.children[index];
continue;
}
this.ensureChildrenIsArray(child);
if (child.props.children.length > 0) listHasAnItem = true;
else delete newChild.props.children[index];
}
if (!listHasAnItem) return null;
newChild.props.children = this.clearEmptyArrayItems(newChild.props.children);
}
return newChild;
};
const modifyChildren = (children: Array<ReactElement<any>>) => {
for (const [index, child] of children.entries()) children[index] = modifyChild(child);
children = this.clearEmptyArrayItems(children);
return children;
};
try {
const newContent = modifyChildren(lodash.cloneDeep(content));
this.trimContent(newContent);
return newContent;
} catch (err) {
new Logger("FakeNitro").error(err);
return content;
} catch (e) {
logger.error("Error transforming AST:", e);
}
return ast;
},
patchFakeNitroStickers(stickers: Array<any>, message: Message) {
@ -722,10 +666,6 @@ export default definePlugin({
});
},
shouldKeepEmojiLink(link: any) {
return link.target && fakeNitroEmojiRegex.test(link.target);
},
addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {
if (!fake) return node;

View file

@ -15,7 +15,8 @@ export default definePlugin({
patches: [
{
find: "unknownUserMentionPlaceholder:",
// small util file
find: "={linkCount:0,onlyLinks:!1};function ",
replacement: {
// SimpleEmbedTypes.has(embed.type) && isEmbedInline(embed)
match: /\i\.has\(\i\.type\)&&\(0,\i\.\i\)\(\i\)/,