Plugin
FakeNitro
Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { addMessagePreEditListener, addMessagePreSendListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents";8
import { definePluginSettings } from "@api/Settings";9
import { ApngBlendOp, ApngDisposeOp, parseAPNG } from "@utils/apng";10
import { Devs } from "@utils/constants";11
import { getCurrentGuild } from "@utils/discord";12
import { Logger } from "@utils/Logger";13
import definePlugin, { OptionType } from "@utils/types";14
import type { Emoji, Message, RenderModalProps, Sticker } from "@vencord/discord-types";15
import { StickerFormatType } from "@vencord/discord-types/enums";16
import { findByCodeLazy, findByPropsLazy, proxyLazyWebpack } from "@webpack";17
import { ChannelStore, ConfirmModal, DraftType, EmojiStore, FluxDispatcher, Forms, GuildMemberStore, IconUtils, lodash, openModal, Parser, PermissionsBits, PermissionStore, StickersStore, UploadHandler, UserSettingsActionCreators, UserSettingsProtoStore, UserStore } from "@webpack/common";18
import { applyPalette, GIFEncoder, quantize } from "gifenc";19
import type { ReactElement, ReactNode } from "react";20
21
const BINARY_READ_OPTIONS = findByPropsLazy("readerFactory");22
23
function searchProtoClassField(localName: string, protoClass: any) {24
const field = protoClass?.fields?.find((field: any) => field.localName === localName);25
if (!field) return;26
27
const fieldGetter = Object.values(field).find(value => typeof value === "function") as any;28
return fieldGetter?.();29
}30
31
const PreloadedUserSettingsActionCreators = proxyLazyWebpack(() => UserSettingsActionCreators.PreloadedUserSettingsActionCreators);32
const AppearanceSettingsActionCreators = proxyLazyWebpack(() => searchProtoClassField("appearance", PreloadedUserSettingsActionCreators.ProtoClass));33
const ClientThemeSettingsActionsCreators = proxyLazyWebpack(() => searchProtoClassField("clientThemeSettings", AppearanceSettingsActionCreators));34
35
const isUnusableRoleSubscriptionEmoji = findByCodeLazy(".getUserIsAdmin(");36
37
const enum EmojiIntentions {38
REACTION,39
STATUS,40
COMMUNITY_CONTENT,41
CHAT,42
GUILD_STICKER_RELATED_EMOJI,43
GUILD_ROLE_BENEFIT_EMOJI,44
COMMUNITY_CONTENT_ONLY,45
SOUNDBOARD,46
VOICE_CHANNEL_TOPIC,47
GIFT,48
AUTO_SUGGESTION,49
POLLS50
}51
52
const IS_BYPASSEABLE_INTENTION = `[${EmojiIntentions.CHAT},${EmojiIntentions.GUILD_STICKER_RELATED_EMOJI}].includes(fakeNitroIntention)`;53
54
const enum FakeNoticeType {55
Sticker,56
Emoji57
}58
59
const fakeNitroEmojiRegex = /\/emojis\/(\d+?)\.(png|webp|gif)/;60
const fakeNitroStickerRegex = /\/stickers\/(\d+?)\./;61
const fakeNitroGifStickerRegex = /\/attachments\/\d+?\/\d+?\/(\d+?)\.gif/;62
const hyperLinkRegex = /\[.+?\]\((https?:\/\/.+?)\)/;63
64
const settings = definePluginSettings({65
enableEmojiBypass: {66
description: "Allows sending fake emojis (also bypasses missing permission to use custom emojis)",67
type: OptionType.BOOLEAN,68
default: true,69
restartNeeded: true70
},71
emojiSize: {72
description: "Size of the emojis when sending",73
type: OptionType.SLIDER,74
default: 48,75
markers: [32, 48, 56, 64, 96, 128, 160, 256, 512]76
},77
transformEmojis: {78
description: "Whether to transform fake emojis into real ones",79
type: OptionType.BOOLEAN,80
default: true,81
restartNeeded: true82
},83
enableStickerBypass: {84
description: "Allows sending fake stickers (also bypasses missing permission to use stickers)",85
type: OptionType.BOOLEAN,86
default: true,87
restartNeeded: true88
},89
stickerSize: {90
description: "Size of the stickers when sending",91
type: OptionType.SLIDER,92
default: 160,93
markers: [32, 64, 128, 160, 256, 512]94
},95
transformStickers: {96
description: "Whether to transform fake stickers into real ones",97
type: OptionType.BOOLEAN,98
default: true,99
restartNeeded: true100
},101
transformCompoundSentence: {102
description: "Whether to transform fake stickers and emojis in compound sentences (sentences with more content than just the fake emoji or sticker link)",103
type: OptionType.BOOLEAN,104
default: false105
},106
enableStreamQualityBypass: {107
description: "Allow streaming in nitro quality",108
type: OptionType.BOOLEAN,109
default: true,110
restartNeeded: true111
},112
useHyperLinks: {113
description: "Whether to use hyperlinks when sending fake emojis and stickers",114
type: OptionType.BOOLEAN,115
default: true116
},117
hyperLinkText: {118
description: "What text the hyperlink should use. {{NAME}} will be replaced with the emoji/sticker name.",119
type: OptionType.STRING,120
default: "{{NAME}}"121
},122
disableEmbedPermissionCheck: {123
description: "Whether to disable the embed permission check when sending fake emojis and stickers",124
type: OptionType.BOOLEAN,125
default: false126
}127
});128
129
function hasPermission(channelId: string, permission: bigint) {130
const channel = ChannelStore.getChannel(channelId);131
132
if (!channel || channel.isPrivate()) return true;133
134
return PermissionStore.can(permission, channel);135
}136
137
const hasExternalEmojiPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_EMOJIS);138
const hasExternalStickerPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.USE_EXTERNAL_STICKERS);139
const hasEmbedPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.EMBED_LINKS);140
const hasAttachmentPerms = (channelId: string) => hasPermission(channelId, PermissionsBits.ATTACH_FILES);141
142
function getWordBoundary(origStr: string, offset: number) {143
return (!origStr[offset] || /\s/.test(origStr[offset])) ? "" : " ";144
}145
146
function CannotEmbedNoticeModal({ modalProps, resolve }: { modalProps: RenderModalProps; resolve: (value: boolean) => void; }) {147
const s = settings.use(["disableEmbedPermissionCheck"]);148
return (149
<ConfirmModal150
{...modalProps}151
title="Hold on!"152
subtitle="You are trying to send/edit a message that contains a FakeNitro emoji or sticker, however you do not have permissions to embed links in the current channel. Are you sure you want to send this message? Your FakeNitro items will appear as a link only."153
confirmText="Send Anyway"154
cancelText="Cancel"155
onConfirm={() => resolve(true)}156
onCloseCallback={() => setImmediate(() => resolve(false))}157
checkboxProps={{158
checked: s.disableEmbedPermissionCheck === true,159
onChange: checked => s.disableEmbedPermissionCheck = checked160
}}161
/>162
);163
}164
165
function showCannotEmbedNotice() {166
return new Promise<boolean>(resolve => {167
openModal(props => <CannotEmbedNoticeModal modalProps={props} resolve={resolve} />);168
});169
}170
171
export default definePlugin({172
name: "FakeNitro",173
authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN, Devs.sadan],174
description: "Allows you to send fake emojis/stickers, use nitro themes, and stream in nitro quality",175
tags: ["Emotes", "Appearance", "Customisation", "Chat"],176
dependencies: ["MessageEventsAPI"],177
178
settings,179
180
patches: [181
{182
find: "canUseCustomStickersEverywhere:",183
replacement: [184
{185
match: /(?<=canUseCustomStickersEverywhere:function\(\i\)\{)/,186
replace: "return true;",187
predicate: () => settings.store.enableStickerBypass188
},189
{190
match: /(?<=canUseHighVideoUploadQuality:function\(\i\)\{)/,191
replace: "return true;",192
predicate: () => settings.store.enableStreamQualityBypass193
},194
{195
match: /(?<=canStreamQuality:function\(\i,\i\)\{)/,196
replace: "return true;",197
predicate: () => settings.store.enableStreamQualityBypass198
},199
{200
match: /(?<=canUseClientThemes:function\(\i\)\{)/,201
replace: "return true;"202
},203
{204
match: /(?<=canUsePremiumAppIcons:function\(\i\)\{)/,205
replace: "return true;"206
}207
],208
},209
// Patch the emoji picker in voice calls to not be bypassed by fake nitro210
{211
find: 039;.getByName("fork_and_knife")039;,212
predicate: () => settings.store.enableEmojiBypass,213
replacement: {214
match: ".CHAT",215
replace: ".STATUS"216
}217
},218
{219
find: ".GUILD_SUBSCRIPTION_UNAVAILABLE;",220
group: true,221
predicate: () => settings.store.enableEmojiBypass,222
replacement: [223
{224
// Create a variable for the intention of using the emoji225
match: /(?<=\.USE_EXTERNAL_EMOJIS.+?;)(?<=intention:(\i).+?)/,226
replace: (_, intention) => `const fakeNitroIntention=${intention};`227
},228
{229
// Disallow the emoji for external if the intention doesn't allow it230
match: /&&!\i&&!\i(?=\)return \i\.\i\.DISALLOW_EXTERNAL;)/,231
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`232
},233
{234
// Disallow the emoji for unavailable if the intention doesn't allow it235
match: /!\i\.available(?=\)return \i\.\i\.GUILD_SUBSCRIPTION_UNAVAILABLE;)/,236
replace: m => `${m}&&!${IS_BYPASSEABLE_INTENTION}`237
},238
{239
// Disallow the emoji for premium locked if the intention doesn't allow it240
match: /!(\i\.\i\.canUseEmojisEverywhere\(\i\))/,241
replace: m => `(${m}&&!${IS_BYPASSEABLE_INTENTION})`242
},243
{244
// Allow animated emojis to be used if the intention allows it245
match: /(?<=\|\|)\i\.\i\.canUseAnimatedEmojis\(\i\)/,246
replace: m => `(${m}||${IS_BYPASSEABLE_INTENTION})`247
}248
]249
},250
// Allows the usage of subscription-locked emojis251
{252
find: ".getUserIsAdmin(",253
replacement: {254
match: /(function \i\(\i,\i)\){(.{0,250}.getUserIsAdmin\(.+?return!1})/,255
replace: (_, rest1, rest2) => `${rest1},fakeNitroOriginal){if(!fakeNitroOriginal)return false;${rest2}`256
}257
},258
// Make stickers always available259
{260
find: 039;"SENDABLE"039;,261
predicate: () => settings.store.enableStickerBypass,262
replacement: {263
match: /\i\.available\?/,264
replace: "true?"265
}266
},267
// Remove boost requirements to stream with high quality268
{269
find: "#{intl::STREAM_FPS_OPTION}",270
predicate: () => settings.store.enableStreamQualityBypass,271
replacement: {272
match: /guildPremiumTier:\i\.\i\.TIER_\d,?/g,273
replace: ""274
}275
},276
{277
find: 039;"UserSettingsProtoStore"039;,278
replacement: [279
{280
// Overwrite incoming connection settings proto with our local settings281
match: /(?<=CONNECTION_OPEN:function\((\i)\){)/,282
replace: (_, props) => `$self.handleProtoChange(${props}.userSettingsProto,${props}.user);`283
},284
{285
// Overwrite non local proto changes with our local settings286
match: /let{settings:/,287
replace: "arguments[0].local||$self.handleProtoChange(arguments[0].settings.proto);$&"288
}289
]290
},291
// Call our function to handle changing the gradient theme when selecting a new one292
{293
find: ",updateTheme(",294
replacement: {295
match: /(function \i\(\i\){let{backgroundGradientPresetId:(\i).+?)(\i\.\i\.updateAsync.+?theme=(.+?),.+?},\i\))/,296
replace: (_, rest, backgroundGradientPresetId, originalCall, theme) => `${rest}$self.handleGradientThemeSelect(${backgroundGradientPresetId},${theme},()=>${originalCall});`297
}298
},299
// Allow users to use custom client themes300
{301
find: 039;("custom_themes_editor_footer")039;,302
replacement: {303
match: /(?<=\i=)\(0,\i\.\i\)\(\i\.\i\.TIER_2\)(?=,|;)/g,304
replace: "true"305
}306
},307
{308
find: 039;["strong","em","u","text","inlineCode","s","spoiler"]039;,309
replacement: [310
{311
// Call our function to decide whether the emoji link should be kept or not312
predicate: () => settings.store.transformEmojis,313
match: /1!==(\i)\.length\|\|1!==\i\.length/,314
replace: (m, content) => `${m}||$self.shouldKeepEmojiLink(${content}[0])`315
},316
{317
// Patch the rendered message content to add fake nitro emojis or remove sticker links318
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,319
match: /(?=return{hasSpoilerEmbeds:\i,hasBailedAst:\i,content:(\i))/,320
replace: (_, content) => `${content}=$self.patchFakeNitroEmojisOrRemoveStickersLinks(${content},arguments[2]?.formatInline);`321
}322
]323
},324
{325
find: "}renderStickersAccessories(",326
replacement: [327
{328
// Call our function to decide whether the embed should be ignored or not329
predicate: () => settings.store.transformEmojis || settings.store.transformStickers,330
match: /(renderEmbeds\((\i)\){)(.+?embeds\.map\(\((\i),\i\)?=>{)/,331
replace: (_, rest1, message, rest2, embed) => `${rest1}const fakeNitroMessage=${message};${rest2}if($self.shouldIgnoreEmbed(${embed},fakeNitroMessage))return null;`332
},333
{334
// Patch the stickers array to add fake nitro stickers335
predicate: () => settings.store.transformStickers,336
match: /renderStickersAccessories\((\i)\){let (\i)=\(0,\i\.\i\)\(\i\).+?;/,337
replace: (m, message, stickers) => `${m}${stickers}=$self.patchFakeNitroStickers(${stickers},${message});`338
},339
{340
// Filter attachments to remove fake nitro stickers or emojis341
predicate: () => settings.store.transformStickers,342
match: /renderAttachments\(\i\){.+?{attachments:(\i).+?;/,343
replace: (m, attachments) => `${m}${attachments}=$self.filterAttachments(${attachments});`344
}345
]346
},347
{348
find: "#{intl::STICKER_POPOUT_UNJOINED_PRIVATE_GUILD_DESCRIPTION}",349
predicate: () => settings.store.transformStickers,350
replacement: [351
{352
// Export the renderable sticker to be used in the fake nitro sticker notice353
match: /let{renderableSticker:(\i).{0,270}sticker:\i,channel:\i,/,354
replace: (m, renderableSticker) => `${m}fakeNitroRenderableSticker:${renderableSticker},`355
},356
{357
// Add the fake nitro sticker notice358
match: /(let \i,{sticker:\i,channel:\i,closePopout:\i.+?}=(\i).+?;)(.+?description:)(\i)(?=,sticker:\i)/,359
replace: (_, rest, props, rest2, reactNode) => `${rest}let{fakeNitroRenderableSticker}=${props};${rest2}$self.addFakeNotice(${FakeNoticeType.Sticker},${reactNode},!!fakeNitroRenderableSticker?.fake)`360
}361
]362
},363
{364
find: ".EMOJI_UPSELL_POPOUT_MORE_EMOJIS_OPENED,",365
predicate: () => settings.store.transformEmojis,366
replacement: {367
// Export the emoji node to be used in the fake nitro emoji notice368
match: /isDiscoverable:\i,shouldHideRoleSubscriptionCTA:\i,(?<={node:(\i),.+?)/,369
replace: (m, node) => `${m}fakeNitroNode:${node},`370
}371
},372
{373
find: "#{intl::EMOJI_POPOUT_UNJOINED_DISCOVERABLE_GUILD_DESCRIPTION}",374
predicate: () => settings.store.transformEmojis,375
replacement: {376
// Add the fake nitro emoji notice377
match: /(?<=emojiDescription:)(\i)(?<=\1=\(\i=>\{.+?\}\)\((\i)\)[,;].+?)/,378
replace: (_, reactNode, props) => `$self.addFakeNotice(${FakeNoticeType.Emoji},${reactNode},!!${props}?.fakeNitroNode?.fake)`379
}380
},381
// Separate patch for allowing using custom app icons382
{383
find: "getCurrentDesktopIcon(),",384
replacement: {385
match: /\i\.\i\.isPremium\(\i\.\i\.getCurrentUser\(\)\)/,386
replace: "true"387
}388
},389
// Make all Soundboard sounds available390
{391
find: 039;type:"GUILD_SOUNDBOARD_SOUND_CREATE"039;,392
replacement: {393
match: /(?<=type:"(?:SOUNDBOARD_SOUNDS_RECEIVED|GUILD_SOUNDBOARD_SOUND_CREATE|GUILD_SOUNDBOARD_SOUND_UPDATE|GUILD_SOUNDBOARD_SOUNDS_UPDATE)".+?available:)\i\.available/g,394
replace: "true"395
}396
}397
],398
399
get guildId() {400
return getCurrentGuild()?.id;401
},402
403
get canUseEmotes() {404
return (UserStore.getCurrentUser().premiumType ?? 0) > 0;405
},406
407
get canUseStickers() {408
return (UserStore.getCurrentUser().premiumType ?? 0) > 1;409
},410
411
handleProtoChange(proto: any, user: any) {412
try {413
if (proto == null || typeof proto === "string") return;414
415
const premiumType: number = user?.premium_type ?? UserStore?.getCurrentUser()?.premiumType ?? 0;416
417
if (premiumType !== 2) {418
proto.appearance ??= AppearanceSettingsActionCreators.create();419
420
const protoStoreAppearenceSettings = UserSettingsProtoStore.settings.appearance;421
422
const appearanceSettingsOverwrite = AppearanceSettingsActionCreators.create({423
...proto.appearance,424
theme: protoStoreAppearenceSettings?.theme,425
clientThemeSettings: protoStoreAppearenceSettings?.clientThemeSettings426
});427
428
proto.appearance = appearanceSettingsOverwrite;429
}430
} catch (err) {431
new Logger("FakeNitro").error(err);432
}433
},434
435
handleGradientThemeSelect(backgroundGradientPresetId: number | undefined, theme: number, original: () => void) {436
const premiumType = UserStore?.getCurrentUser()?.premiumType ?? 0;437
if (premiumType === 2 || backgroundGradientPresetId == null) return original();438
439
if (!PreloadedUserSettingsActionCreators || !AppearanceSettingsActionCreators || !ClientThemeSettingsActionsCreators || !BINARY_READ_OPTIONS) return;440
441
const currentAppearanceSettings = PreloadedUserSettingsActionCreators.getCurrentValue().appearance;442
443
const newAppearanceProto = currentAppearanceSettings != null444
? AppearanceSettingsActionCreators.fromBinary(AppearanceSettingsActionCreators.toBinary(currentAppearanceSettings), BINARY_READ_OPTIONS)445
: AppearanceSettingsActionCreators.create();446
447
newAppearanceProto.theme = theme;448
449
const clientThemeSettingsDummy = ClientThemeSettingsActionsCreators.create({450
backgroundGradientPresetId: {451
value: backgroundGradientPresetId452
}453
});454
455
newAppearanceProto.clientThemeSettings ??= clientThemeSettingsDummy;456
newAppearanceProto.clientThemeSettings.backgroundGradientPresetId = clientThemeSettingsDummy.backgroundGradientPresetId;457
458
const proto = PreloadedUserSettingsActionCreators.ProtoClass.create();459
proto.appearance = newAppearanceProto;460
461
FluxDispatcher.dispatch({462
type: "USER_SETTINGS_PROTO_UPDATE",463
local: true,464
partial: true,465
settings: {466
type: 1,467
proto468
}469
});470
},471
472
trimContent(content: Array<any>) {473
const firstContent = content[0];474
if (typeof firstContent === "string") {475
content[0] = firstContent.trimStart();476
content[0] || content.shift();477
} else if (typeof firstContent?.props?.children === "string") {478
firstContent.props.children = firstContent.props.children.trimStart();479
firstContent.props.children || content.shift();480
}481
482
const lastIndex = content.length - 1;483
const lastContent = content[lastIndex];484
if (typeof lastContent === "string") {485
content[lastIndex] = lastContent.trimEnd();486
content[lastIndex] || content.pop();487
} else if (typeof lastContent?.props?.children === "string") {488
lastContent.props.children = lastContent.props.children.trimEnd();489
lastContent.props.children || content.pop();490
}491
},492
493
clearEmptyArrayItems(array: Array<any>) {494
return array.filter(item => item != null);495
},496
497
ensureChildrenIsArray(child: ReactElement<any>) {498
if (!Array.isArray(child.props.children)) child.props.children = [child.props.children];499
},500
501
patchFakeNitroEmojisOrRemoveStickersLinks(content: Array<any>, inline: boolean) {502
// If content has more than one child or it's a single ReactElement like a header, list or span503
if ((content.length > 1 || typeof content[0]?.type === "string") && !settings.store.transformCompoundSentence) return content;504
505
let nextIndex = content.length;506
507
const transformLinkChild = (child: ReactElement<any>) => {508
if (settings.store.transformEmojis) {509
const fakeNitroMatch = child.props.href.match(fakeNitroEmojiRegex);510
if (fakeNitroMatch) {511
let url: URL | null = null;512
try {513
url = new URL(child.props.href);514
} catch { }515
516
const emojiName = EmojiStore.getCustomEmojiById(fakeNitroMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroEmoji";517
const isAnimated = fakeNitroMatch[2] === "gif" || url?.searchParams.get("animated") === "true";518
519
return Parser.defaultRules.customEmoji.react({520
jumboable: !inline && content.length === 1 && typeof content[0].type !== "string",521
animated: isAnimated,522
emojiId: fakeNitroMatch[1],523
name: emojiName,524
fake: true525
}, void 0, { key: String(nextIndex++) });526
}527
}528
529
if (settings.store.transformStickers) {530
if (fakeNitroStickerRegex.test(child.props.href)) return null;531
532
const gifMatch = child.props.href.match(fakeNitroGifStickerRegex);533
if (gifMatch) {534
// 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 sticker535
if (StickersStore.getStickerById(gifMatch[1])) return null;536
}537
}538
539
return child;540
};541
542
const transformChild = (child: ReactElement<any>) => {543
if (child?.props?.trusted != null) return transformLinkChild(child);544
if (child?.props?.children != null) {545
if (!Array.isArray(child.props.children)) {546
child.props.children = modifyChild(child.props.children);547
return child;548
}549
550
child.props.children = modifyChildren(child.props.children);551
if (child.props.children.length === 0) return null;552
return child;553
}554
555
return child;556
};557
558
const modifyChild = (child: ReactElement<any>) => {559
const newChild = transformChild(child);560
561
if (newChild?.type === "ul" || newChild?.type === "ol") {562
this.ensureChildrenIsArray(newChild);563
if (newChild.props.children.length === 0) return null;564
565
let listHasAnItem = false;566
for (const [index, child] of newChild.props.children.entries()) {567
if (child == null) {568
delete newChild.props.children[index];569
continue;570
}571
572
this.ensureChildrenIsArray(child);573
if (child.props.children.length > 0) listHasAnItem = true;574
else delete newChild.props.children[index];575
}576
577
if (!listHasAnItem) return null;578
579
newChild.props.children = this.clearEmptyArrayItems(newChild.props.children);580
}581
582
return newChild;583
};584
585
const modifyChildren = (children: Array<ReactElement<any>>) => {586
for (const [index, child] of children.entries()) children[index] = modifyChild(child);587
588
children = this.clearEmptyArrayItems(children);589
590
return children;591
};592
593
try {594
const newContent = modifyChildren(lodash.cloneDeep(content));595
this.trimContent(newContent);596
597
return newContent;598
} catch (err) {599
new Logger("FakeNitro").error(err);600
return content;601
}602
},603
604
patchFakeNitroStickers(stickers: Array<any>, message: Message) {605
const itemsToMaybePush: Array<string> = [];606
607
const contentItems = message.content.split(/\s/);608
if (settings.store.transformCompoundSentence) itemsToMaybePush.push(...contentItems);609
else if (contentItems.length === 1) itemsToMaybePush.push(contentItems[0]);610
611
itemsToMaybePush.push(...message.attachments.filter(attachment => attachment.content_type === "image/gif").map(attachment => attachment.url));612
613
for (const item of itemsToMaybePush) {614
if (!settings.store.transformCompoundSentence && !item.startsWith("http") && !hyperLinkRegex.test(item)) continue;615
616
const imgMatch = item.match(fakeNitroStickerRegex);617
if (imgMatch) {618
let url: URL | null = null;619
try {620
url = new URL(item);621
} catch { }622
623
const stickerName = StickersStore.getStickerById(imgMatch[1])?.name ?? url?.searchParams.get("name") ?? "FakeNitroSticker";624
stickers.push({625
format_type: 1,626
id: imgMatch[1],627
name: stickerName,628
fake: true629
});630
631
continue;632
}633
634
const gifMatch = item.match(fakeNitroGifStickerRegex);635
if (gifMatch) {636
if (!StickersStore.getStickerById(gifMatch[1])) continue;637
638
const stickerName = StickersStore.getStickerById(gifMatch[1])?.name ?? "FakeNitroSticker";639
stickers.push({640
format_type: 2,641
id: gifMatch[1],642
name: stickerName,643
fake: true644
});645
}646
}647
648
return stickers;649
},650
651
shouldIgnoreEmbed(embed: Message["embeds"][number], message: Message) {652
try {653
const contentItems = message.content.split(/\s/);654
if (contentItems.length > 1 && !settings.store.transformCompoundSentence) return false;655
656
switch (embed.type) {657
case "image": {658
const url = embed.url ?? embed.image?.url;659
if (!url) return false;660
if (661
!settings.store.transformCompoundSentence662
&& !contentItems.some(item => item === url || item.match(hyperLinkRegex)?.[1] === url)663
) return false;664
665
if (settings.store.transformEmojis) {666
if (fakeNitroEmojiRegex.test(url)) return true;667
}668
669
if (settings.store.transformStickers) {670
if (fakeNitroStickerRegex.test(url)) return true;671
672
const gifMatch = url.match(fakeNitroGifStickerRegex);673
if (gifMatch) {674
// 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 sticker675
if (StickersStore.getStickerById(gifMatch[1])) return true;676
}677
}678
679
break;680
}681
}682
} catch (e) {683
new Logger("FakeNitro").error("Error in shouldIgnoreEmbed:", e);684
}685
686
return false;687
},688
689
filterAttachments(attachments: Message["attachments"]) {690
return attachments.filter(attachment => {691
if (attachment.content_type !== "image/gif") return true;692
693
const match = attachment.url.match(fakeNitroGifStickerRegex);694
if (match) {695
// 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 sticker696
if (StickersStore.getStickerById(match[1])) return false;697
}698
699
return true;700
});701
},702
703
shouldKeepEmojiLink(link: any) {704
return link.target && fakeNitroEmojiRegex.test(link.target);705
},706
707
addFakeNotice(type: FakeNoticeType, node: Array<ReactNode>, fake: boolean) {708
if (!fake) return node;709
710
node = Array.isArray(node) ? node : [node];711
712
switch (type) {713
case FakeNoticeType.Sticker: {714
node.push(" This is a FakeNitro sticker and renders like a real sticker only for you. Appears as a link to non-plugin users.");715
716
return node;717
}718
case FakeNoticeType.Emoji: {719
node.push(" This is a FakeNitro emoji and renders like a real emoji only for you. Appears as a link to non-plugin users.");720
721
return node;722
}723
}724
},725
726
getStickerLink({ format_type, id }: Sticker) {727
const ext = format_type === StickerFormatType.GIF ? "gif" : "png";728
return `https:class="ts-cmt">//media.discordapp.net/stickers/${id}.${ext}?size=${settings.store.stickerSize}`;729
},730
731
async sendAnimatedSticker(stickerLink: string, stickerId: string, channelId: string) {732
733
const { frames, width, height } = await fetch(stickerLink)734
.then(res => res.arrayBuffer())735
.then(parseAPNG);736
737
const gif = GIFEncoder();738
const resolution = settings.store.stickerSize;739
740
const canvas = document.createElement("canvas");741
canvas.width = resolution;742
canvas.height = resolution;743
744
const ctx = canvas.getContext("2d", {745
willReadFrequently: true746
})!;747
748
const scale = resolution / Math.max(width, height);749
ctx.scale(scale, scale);750
751
let previousFrameData: ImageData;752
753
for (const frame of frames) {754
const { left, top, width, height, img, delay, blendOp, disposeOp } = frame;755
756
previousFrameData = ctx.getImageData(left, top, width, height);757
758
if (blendOp === ApngBlendOp.SOURCE) {759
ctx.clearRect(left, top, width, height);760
}761
762
ctx.drawImage(img, left, top, width, height);763
764
const { data } = ctx.getImageData(0, 0, resolution, resolution);765
766
const palette = quantize(data, 256);767
const index = applyPalette(data, palette);768
769
gif.writeFrame(index, resolution, resolution, {770
transparent: true,771
palette,772
delay773
});774
775
if (disposeOp === ApngDisposeOp.BACKGROUND) {776
ctx.clearRect(left, top, width, height);777
} else if (disposeOp === ApngDisposeOp.PREVIOUS) {778
ctx.putImageData(previousFrameData, left, top);779
}780
}781
782
gif.finish();783
784
const file = new File([gif.bytesView() as Uint8Array<ArrayBuffer>], `${stickerId}.gif`, { type: "image/gif" });785
UploadHandler.promptToUpload([file], ChannelStore.getChannel(channelId), DraftType.ChannelMessage);786
},787
788
canUseEmote(e: Emoji, channelId: string) {789
if (e.type === 0) return true;790
if (e.available === false) return false;791
792
if (isUnusableRoleSubscriptionEmoji(e, this.guildId, true)) return false;793
794
let isUsableTwitchSubEmote = false;795
if (e.managed && e.guildId) {796
const myRoles = GuildMemberStore.getSelfMember(e.guildId)?.roles ?? [];797
isUsableTwitchSubEmote = e.roles.some(r => myRoles.includes(r));798
}799
800
if (this.canUseEmotes || isUsableTwitchSubEmote)801
return e.guildId === this.guildId || hasExternalEmojiPerms(channelId);802
else803
return !e.animated && e.guildId === this.guildId;804
},805
806
start() {807
const s = settings.store;808
809
if (!s.enableEmojiBypass && !s.enableStickerBypass) {810
return;811
}812
813
this.preSend = addMessagePreSendListener(async (channelId, messageObj, extra) => {814
const { guildId } = this;815
816
let hasBypass = false;817
818
stickerBypass: {819
if (!s.enableStickerBypass)820
break stickerBypass;821
822
const sticker = StickersStore.getStickerById(extra.stickers?.[0]!);823
if (!sticker)824
break stickerBypass;825
826
// Discord Stickers are now free yayyy!! :D827
if ("pack_id" in sticker)828
break stickerBypass;829
830
const canUseStickers = this.canUseStickers && hasExternalStickerPerms(channelId);831
if (sticker.available !== false && (canUseStickers || sticker.guild_id === guildId))832
break stickerBypass;833
834
const link = this.getStickerLink(sticker);835
836
if (sticker.format_type === StickerFormatType.APNG) {837
if (!hasAttachmentPerms(channelId)) {838
openModal(props => (839
<ConfirmModal840
{...props}841
title="Hold on!"842
confirmText="OK"843
variant="primary"844
>845
<div>846
<Forms.FormText>847
You cannot send this message because it contains an animated FakeNitro sticker,848
and you do not have permissions to attach files in the current channel. Please remove the sticker to proceed.849
</Forms.FormText>850
</div>851
</ConfirmModal>852
));853
} else {854
this.sendAnimatedSticker(link, sticker.id, channelId);855
}856
857
return { cancel: true };858
} else {859
hasBypass = true;860
861
const url = new URL(link);862
url.searchParams.set("name", sticker.name);863
url.searchParams.set("lossless", "true");864
865
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", sticker.name);866
867
messageObj.content += `${getWordBoundary(messageObj.content, messageObj.content.length - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}`;868
extra.stickers!.length = 0;869
}870
}871
872
if (s.enableEmojiBypass) {873
for (const emoji of messageObj.validNonShortcutEmojis) {874
if (this.canUseEmote(emoji, channelId)) continue;875
876
hasBypass = true;877
878
const emojiString = `<${emoji.animated ? "a" : ""}:${emoji.originalName || emoji.name}:${emoji.id}>`;879
880
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));881
url.searchParams.set("size", s.emojiSize.toString());882
url.searchParams.set("name", emoji.name);883
url.searchParams.set("lossless", "true");884
885
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name);886
887
messageObj.content = messageObj.content.replace(emojiString, (match, offset, origStr) => {888
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + match.length)}`;889
});890
}891
}892
893
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {894
if (!await showCannotEmbedNotice()) {895
return { cancel: true };896
}897
}898
899
return { cancel: false };900
});901
902
this.preEdit = addMessagePreEditListener(async (channelId, __, messageObj) => {903
if (!s.enableEmojiBypass) return;904
905
let hasBypass = false;906
907
messageObj.content = messageObj.content.replace(/(?<!\\)<a?:(?:\w+):(\d+)>/ig, (emojiStr, emojiId, offset, origStr) => {908
const emoji = EmojiStore.getCustomEmojiById(emojiId);909
if (emoji == null) return emojiStr;910
if (this.canUseEmote(emoji, channelId)) return emojiStr;911
912
hasBypass = true;913
914
const url = new URL(IconUtils.getEmojiURL({ id: emoji.id, animated: emoji.animated, size: s.emojiSize }));915
url.searchParams.set("size", s.emojiSize.toString());916
url.searchParams.set("name", emoji.name);917
url.searchParams.set("lossless", "true");918
919
const linkText = s.hyperLinkText.replaceAll("{{NAME}}", emoji.name);920
921
return `${getWordBoundary(origStr, offset - 1)}${s.useHyperLinks ? `[${linkText}](${url})` : url}${getWordBoundary(origStr, offset + emojiStr.length)}`;922
});923
924
if (hasBypass && !s.disableEmbedPermissionCheck && !hasEmbedPerms(channelId)) {925
if (!await showCannotEmbedNotice()) {926
return { cancel: true };927
}928
}929
930
return { cancel: false };931
});932
},933
934
stop() {935
removeMessagePreSendListener(this.preSend);936
removeMessagePreEditListener(this.preEdit);937
}938
});939