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