/**
* @name BDNitro
* @author SrGobi
* @authorLink https://github.com/srgobi
* @version 6.1.4
* @invite cqrN3Eg
* @source https://github.com/srgobi/BDNitro
* @donate https://github.com/srgobi/BDNitro?tab=readme-ov-file#donate
* @updateUrl https://raw.githubusercontent.com/srgobi/BDNitro/main/BDNitro.plugin.js
* @description Unlock all screensharing modes, use cross-server & GIF emotes, and more!
*/
/*@cc_on
@if(@_jscript)
// Offer to self-install for clueless users that try to run this directly.
var shell = WScript.CreateObject("WScript.Shell");
var fs = new ActiveXObject("Scripting.FileSystemObject");
var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\\BetterDiscord\\plugins");
var pathSelf = WScript.ScriptFullName;
// Put the user at ease by addressing them in the first person
shell.Popup("It looks like you've mistakenly tried to run me directly. \n(Don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
if(fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)){
shell.Popup("I'm in the correct folder already.", 0, "I'm already installed", 0x40);
}else if(!fs.FolderExists(pathPlugins)){
shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
}else if(shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6){
fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
// Show the user where to put plugins in the future
shell.Exec("explorer " + pathPlugins);
shell.Popup("I'm installed!", 0, "Successfully installed", 0x40);
}
WScript.Quit();
@else@*/
/* ***** ATTRIBUTION NOTICE *****
*
* BDNitro is a free BetterDiscord plugin that bypasses and unlocks Nitro-locked features in the Discord client.
*
* Copyright (c) 2025 srgobi and contributors
*
* Licensed under the Non-Profit Open Software License version 3.0 (NPOSL-3.0).
* You may use, distribute, and modify this code under the terms of this license.
*
* Derivative works must be licensed under NPOSL-3.0 (or OSL-3.0 for for-profit use).
*
* Removal or modification of this notice in the source code of any Derivative Work
* of this software violates the terms of the license.
*
* This software is provided on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied,
* including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose.
* THE ENTIRE RISK AS TO THE QUALITY OF THIS SOFTWARE IS WITH YOU.
*
* You should have received a copy of the license agreement alongside this file.
* If not, please visit https://github.com/srgobi/BDNitro/blob/main/LICENSE.md
*
*/
//#region Module Hell
const { Webpack, Patcher, Net, React, UI, Logger, Data, Components, DOM, Plugins } = BdApi;
const StreamButtons = Webpack.getMangled('RESOLUTION_1080', {
ApplicationStreamFPS: Webpack.Filters.byKeys('FPS_30'),
ApplicationStreamFPSButtons: (o) => Array.isArray(o) && typeof o[0]?.label === 'number' && o[0]?.value === 15,
ApplicationStreamFPSButtonsWithSuffixLabel: (o) => Array.isArray(o) && typeof o[0]?.label === 'string' && o[0]?.value === 15,
ApplicationStreamResolutionButtons: (o) => Array.isArray(o) && o[0]?.value !== undefined,
ApplicationStreamResolutionButtonsWithSuffixLabel: (o) => Array.isArray(o) && o[0]?.label === '480p',
ApplicationStreamResolutions: Webpack.Filters.byKeys('RESOLUTION_1080')
});
const { ApplicationStreamFPS, ApplicationStreamFPSButtons, ApplicationStreamFPSButtonsWithSuffixLabel, ApplicationStreamResolutionButtons, ApplicationStreamResolutionButtonsWithSuffixLabel, ApplicationStreamResolutions } = StreamButtons;
const CloudUploader = Webpack.getModule(Webpack.Filters.byPrototypeKeys('uploadFileToCloud'), { searchExports: true });
const UserStore = Webpack.getStore('UserStore');
const CurrentUser = UserStore.getCurrentUser();
const ORIGINAL_NITRO_STATUS = CurrentUser.premiumType;
const getBannerURL = Webpack.getByPrototypeKeys('getBannerURL').prototype;
const UserProfileStore = Webpack.getStore('UserProfileStore');
const buttonClassModule = Webpack.getByKeys('lookFilled', 'button', 'contents');
const Dispatcher = Webpack.getByKeys('subscribe', 'dispatch');
const canUserUseMod = Webpack.getMangled('.getFeatureValue(', {
canUserUse: Webpack.Filters.byStrings('getFeatureValue')
});
const AvatarDefaults = Webpack.getByKeys('getEmojiURL');
const LadderModule = Webpack.getModule(Webpack.Filters.byKeys('calculateLadder'), { searchExports: true });
const FetchCollectibleCategories = Webpack.getByStrings('{type:"COLLECTIBLES_CATEGORIES_FETCH"', { searchExports: true });
let ffmpeg = undefined;
const udta = new Uint8Array([
0, 0, 0, 89, 109, 101, 116, 97, 0, 0, 0, 0, 0, 0, 0, 33, 104, 100, 108, 114, 0, 0, 0, 0, 0, 0, 0, 0, 109, 100, 105, 114, 97, 112, 112, 108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 105, 108, 115, 116, 0, 0, 0, 36, 169, 116, 111, 111, 0, 0, 0, 28, 100, 97, 116, 97, 0, 0, 0, 1, 0, 0, 0, 0, 76, 97, 118, 102, 54, 49, 46, 51, 46, 49, 48, 51, 0, 0, 46, 46, 117, 117, 105, 100, 161, 200, 82, 153, 51, 70, 77, 184, 136, 240,
131, 245, 122, 117, 165, 239
]);
const udtaBuffer = udta.buffer;
const PresenceStore = Webpack.getStore('PresenceStore');
const SelectedGuildStore = Webpack.getStore('SelectedGuildStore');
const ChannelStore = Webpack.getStore('ChannelStore');
const MessageActions = Webpack.getByKeys('jumpToMessage', '_sendMessage');
const SelectedChannelStore = Webpack.getStore('SelectedChannelStore');
const MessageEmojiReact = Webpack.getByStrings(',nudgeAlignIntoViewport:!0,position:', 'jumboable?', { searchExports: true });
const renderEmbedsMod = Webpack.getByPrototypeKeys('renderSocialProofingFileSizeNitroUpsell', { searchExports: true }).prototype;
const messageRender = Webpack.getMangled('.SEND_FAILED,', {
renderMessage: (o) => typeof o === 'object'
});
const stickerSendabilityModule = Webpack.getMangled('SENDABLE_WITH_BOOSTED_GUILD', {
getStickerSendability: Webpack.Filters.byStrings('canUseCustomStickersEverywhere'),
isSendableSticker: Webpack.Filters.byStrings(')=>0===')
});
const clientThemesModule = Webpack.getModule(Webpack.Filters.byKeys('isPreview'));
const streamSettingsMod = Webpack.getByPrototypeKeys('getCodecOptions').prototype;
const themesModule = Webpack.getMangled('changes:{appearance:{settings:{clientThemeSettings:{', {
saveClientTheme: Webpack.Filters.byStrings('changes:{appearance:{settings:{clientThemeSettings:{')
});
const accountSwitchModule = Webpack.getByKeys('startSession', 'login');
const getAvatarUrlModule = Webpack.getByPrototypeKeys('getAvatarURL').prototype;
const fetchProfileEffects = Webpack.getByStrings('USER_PROFILE_EFFECTS_FETCH', { searchExports: true });
const SoundboardStore = Webpack.getStore('SoundboardStore');
const EmojiStore = Webpack.getStore('EmojiStore');
const isEmojiAvailableMod = Webpack.getByKeys('isEmojiFilteredOrLocked');
const TextClasses = Webpack.getByKeys('errorMessage', 'h5');
const videoOptionFunctions = Webpack.getByPrototypeKeys('updateVideoQuality').prototype;
const appIconButtonsModule = Webpack.getMangled(/isEditor:.{1,3},renderCTAButtons/, {
CTAButtons: (x) => x
});
const addFilesMod = Webpack.getByKeys('addFiles');
const AppIcon = Webpack.getMangled('AppIconHome', {
AppIconHome: (x) => x
});
const RegularAppIcon = Webpack.getByStrings('M19.73 4.87a18.2', { searchExports: true });
const CurrentDesktopIcon = Webpack.getStore('AppIconPersistedStoreState');
const CustomAppIcon = Webpack.getByStrings('.iconSource,width:');
const ClipsEnabledMod = Webpack.getMangled('useExperiment({location:"useEnableClips"', {
useEnableClips: Webpack.Filters.byStrings('useExperiment({location:"useEnableClips"'),
areClipsEnabled: Webpack.Filters.byStrings('areClipsEnabled'),
isPremium: Webpack.Filters.byStrings('isPremiumAtLeast')
});
const ClipsAllowedMod = Webpack.getMangled(`let{ignorePlatformRestriction:`, {
isClipsClientCapable: (x) => x == x //just get the first result lol
});
const ClipsStore = Webpack.getStore('ClipsStore');
const MaxFileSizeMod = Webpack.getMangled('.premiumTier].limits.fileSize:', {
getMaxFileSize: Webpack.Filters.byStrings('.premiumTier].limits.fileSize:'),
exceedsMessageSizeLimit: Webpack.Filters.byStrings('Array.from(', '.size>')
});
const InvalidStreamSettingsModal = Webpack.getMangled(/\.preset\)&&.{1,3}?===.{1,3}?resolution&&/, {
areStreamSettingsAllowed: (x) => x
});
const GoLiveModalV2UpsellMod = Webpack.getMangled('onNitroClick:function', {
GoLiveModalV2Upsell: (x) => x == x
});
const fs = require('fs');
const path = require('path');
const NameplateSectionMod = Webpack.getMangled(/\{pendingNameplate:.{1,3}?,pendingErrors:.{1,3}?\}=\(/, {
NameplateSection: (x) => x
});
const UserSettingsAccountStore = Webpack.getStore('UserSettingsAccountStore');
const NameplatePalettes = Webpack.getBySource('Crimson', 'darkBackground', 'lightBackground', { searchExports: true });
const NameplatePreview = Webpack.getByRegex(/user:.{1,3}?,nameplate:.{1,3}?,nameplateData:/);
//#endregion
// Calc CRC32 Table
const crcTable = Array.from({ length: 256 }, (_, i) => Array.from({ length: 8 }, (_, j) => j).reduce((crc) => (crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1), i));
const defaultSettings = {
emojiSize: 64,
screenSharing: true,
emojiBypass: true,
emojiBypassType: 0,
emojiBypassForValidEmoji: true,
PNGemote: true,
uploadStickers: false,
CustomFPSEnabled: false,
CustomFPS: 60,
ResolutionEnabled: false,
CustomResolution: 1440,
CustomBitrateEnabled: false,
minBitrate: -1,
maxBitrate: -1,
targetBitrate: -1,
voiceBitrate: -1,
ResolutionSwapper: true,
stickerBypass: false,
profileV2: false,
forceStickersUnlocked: false,
changePremiumType: false,
videoCodec2: -1,
clientThemes: true,
lastGradientSettingStore: -1,
fakeProfileThemes: true,
removeProfileUpsell: false,
removeScreenshareUpsell: true,
fakeProfileBanners: true,
fakeAvatarDecorations: true,
unlockAppIcons: false,
profileEffects: true,
killProfileEffects: false,
customPFPs: true,
experiments: false,
userPfpIntegration: true,
userBgIntegration: true,
useClipBypass: true,
alwaysTransmuxClips: false,
forceClip: false,
checkForUpdates: true,
fakeInlineVencordEmotes: true,
soundmojiEnabled: true,
useAudioClipBypass: true,
forceAudioClip: false,
zipClip: true,
enableClipsExperiment: true,
disableUserBadge: false,
nameplatesEnabled: true
};
const defaultData = {
avatarDecorations: {},
nameplatesV2: {}
};
//Plugin-wide variables
let settings = {};
let data = {};
let badgeUserIDs = [];
let fetchedUserBg = false;
let fetchedUserPfp = false;
// #region Config
const config = {
info: {
name: 'BDNitro',
authors: [
{
name: 'srgobi',
discord_id: '359063827091816448',
github_username: 'srgobi'
}
],
version: '6.1.4',
description: 'Unlock all screensharing modes, and use cross-server & GIF emotes!',
github: 'https://github.com/srgobi/BDNitro',
github_raw: 'https://raw.githubusercontent.com/srgobi/BDNitro/main/BDNitro.plugin.js'
},
changelog: [
{
title: '6.1.4',
items: ['Fixed Fake Profile Themes Copy 3y3 Button not appearing due to a typo.', 'Fixed regression causing Fake Profile Themes colors to not be copied in certain scenarios.']
}
],
settingsPanel: [
{
type: 'category',
id: 'Badges',
name: 'Badges',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'certified_moderator', name: 'Moderador Certificado', note: 'Desbloquea el badge de exalumnos de la academia de moderadores', value: () => settings.certified_moderator },
{ type: 'switch', id: 'hypesquad', name: 'Eventos del HypeSquad', note: 'Desbloquea el badge de eventos del HypeSquad', value: () => settings.hypesquad },
{ type: 'switch', id: 'hypesquad_house_2', name: 'House Brilliance', note: 'Desbloquea el badge de contribuidor', value: () => settings.hypesquad_house_2 },
{ type: 'switch', id: 'hypesquad_house_1', name: 'House Bravery', note: 'Desbloquea el badge de contribuidor', value: () => settings.hypesquad_house_1 },
{ type: 'switch', id: 'hypesquad_house_3', name: 'House Balance', note: 'Desbloquea el badge de contribuidor', value: () => settings.hypesquad_house_3 },
{ type: 'switch', id: 'bug_hunter_level_1', name: 'Bug Hunter', note: 'Desbloquea el badge de cazador de bugs', value: () => settings.bug_hunter_level_1 },
{ type: 'switch', id: 'verified_developer', name: 'Early Bot Developer', note: 'Desbloquea el badge de desarrollador de bots temprano', value: () => settings.verified_developer },
{ type: 'switch', id: 'NITRO', name: 'Nitro', note: 'Desbloquea el badge de Nitro', value: () => settings.NITRO },
{ type: 'switch', id: 'early_supporter', name: 'Early Supporter', note: 'Desbloquea el badge de Early Supporter', value: () => settings.early_supporter },
{
type: 'dropdown',
id: 'nitroBadge',
name: 'Badge de Nitro',
note: 'Selecciona un badge de Nitro. Solo puedes activar uno a la vez.',
value: () => settings.nitroBadge,
options: [
{ label: 'Ninguno', value: '' },
{ label: 'Bronce', value: 'bronze' },
{ label: 'Plata', value: 'silver' },
{ label: 'Oro', value: 'gold' },
{ label: 'Platino', value: 'platinum' },
{ label: 'Diamante', value: 'diamond' },
{ label: 'Esmeralda', value: 'emerald' },
{ label: 'Rubí', value: 'ruby' },
{ label: 'Ópalo', value: 'opal' }
]
}
]
},
{
type: 'category',
id: 'ScreenShare',
name: 'Screen Share Features',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'screenSharing', name: 'High Quality Screensharing', note: '1080p/Source @ 60fps screensharing. Enable if you want to use any Screen Share related options.', value: () => settings.screenSharing },
{ type: 'switch', id: 'ResolutionEnabled', name: 'Custom Screenshare Resolution', note: 'Choose your own screen share resolution!', value: () => settings.ResolutionEnabled },
{ type: 'text', id: 'CustomResolution', name: 'Resolution', note: 'The custom resolution you want (in pixels)', value: () => settings.CustomResolution },
{ type: 'switch', id: 'CustomFPSEnabled', name: 'Custom Screenshare FPS', note: 'Choose your own screen share FPS!', value: () => settings.CustomFPSEnabled },
{ type: 'text', id: 'CustomFPS', name: 'FPS', note: 'The custom FPS you want to stream at.', value: () => settings.CustomFPS },
{ type: 'switch', id: 'ResolutionSwapper', name: 'Stream Settings Quick Swapper', note: 'Lets you change your custom resolution and FPS quickly in the stream settings modal!', value: () => settings.ResolutionSwapper },
{ type: 'switch', id: 'CustomBitrateEnabled', name: 'Custom Bitrate', note: 'Choose the bitrate for your streams!', value: () => settings.CustomBitrateEnabled },
{ type: 'text', id: 'minBitrate', name: 'Minimum Bitrate', note: 'The minimum bitrate (in kbps). If this is set to a negative number, the default for your quality choices is used.', value: () => settings.minBitrate },
{ type: 'text', id: 'targetBitrate', name: 'Target Bitrate', note: 'The target bitrate (in kbps). If this is set to a negative number, the default for your quality choices is used.', value: () => settings.targetBitrate },
{
type: 'text',
id: 'maxBitrate',
name: 'Maximum Bitrate',
note: `The maximum bitrate (in kbps). If this is set to a negative number, the default for your quality choices is used.
The default max bitrate for free quality options is 3500kbps, and for Nitro quality options (higher than 720p or higher than 30fps) it is 9000kbps as of April 2025.
There is also a strange bug(?) where setting your max bitrate will cause issues with your stream's preview.
If you want to avoid these issues, please disable this option.`,
value: () => settings.maxBitrate
},
{
type: 'text',
id: 'voiceBitrate',
name: 'Voice Audio Bitrate',
note: `
Allows you to change the voice bitrate to whatever you want.
Does not allow you to go over the voice channel's set bitrate but it does allow you to go much lower.
Bitrate in kbps. Disabled if this is set to -1.`,
value: () => settings.voiceBitrate
},
{
type: 'dropdown',
id: 'videoCodec2',
name: 'Force Video Codec (Advanced Users Only)',
note: `
Allows you to force a specified video codec to be used. Normally, Discord would automatically
choose this based on your hardware, options in Voice & Video, and the viewers watching.
Mobile and Web clients can only view H.264 and VP8 streams.
If a client does not support the codec you choose, the stream will infinitely load for them!`,
value: () => settings.videoCodec2,
options: [
{ label: 'Default (recommended, automatic)', value: -1 },
{ label: 'AV1', value: 0 },
{ label: 'H265', value: 1 },
{ label: 'H264', value: 2 },
{ label: 'VP8', value: 3 },
{ label: 'VP9', value: 4 }
]
}
]
},
{
type: 'category',
id: 'emojis',
name: 'Emojis',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'emojiBypass', name: 'Nitro Emotes Bypass', note: 'Enable or disable using the emoji bypass.', value: () => settings.emojiBypass },
{
type: 'dropdown',
id: 'emojiSize',
name: 'Size',
note: 'The size of the emoji in pixels.',
value: () => settings.emojiSize,
options: [
{ label: '32px (Default small/inline)', value: 32 },
{ label: '48px (Recommended, default large)', value: 48 },
{ label: '16px', value: 16 },
{ label: '24px', value: 24 },
{ label: '40px', value: 40 },
{ label: '56px', value: 56 },
{ label: '64px', value: 64 },
{ label: '80px', value: 80 },
{ label: '96px', value: 96 },
{ label: '128px (Max emoji size)', value: 128 },
{ label: '256px (Max GIF emoji size)', value: 256 }
]
},
{
type: 'dropdown',
id: 'emojiBypassType',
name: 'Emoji Bypass Method',
note: 'The method of bypass to use.',
value: () => settings.emojiBypassType,
options: [
{ label: 'Upload Emojis', value: 0 },
{ label: 'Ghost Link Mode', value: 1 },
{ label: 'Classic Mode', value: 2 },
{ label: 'Hyperlink/Vencord-Like Mode', value: 3 }
]
},
{ type: 'switch', id: 'emojiBypassForValidEmoji', name: "Don't Use Emote Bypass if Emote is Unlocked", note: 'Disable to use emoji bypass even if bypass is not required for that emoji.', value: () => settings.emojiBypassForValidEmoji },
{ type: 'switch', id: 'PNGemote', name: 'Use PNG instead of WEBP', note: 'Use the PNG version of static emoji for higher quality!', value: () => settings.PNGemote },
{ type: 'switch', id: 'stickerBypass', name: 'Sticker Bypass', note: "Enable or disable using the sticker bypass. I recommend using An00nymushun's DiscordFreeStickers over this. Animated APNG/WEBP/Lottie Stickers WILL NOT animate.", value: () => settings.stickerBypass },
{ type: 'switch', id: 'uploadStickers', name: 'Upload Stickers', note: 'Upload stickers in the same way as emotes.', value: () => settings.uploadStickers },
{ type: 'switch', id: 'forceStickersUnlocked', name: 'Force Stickers Unlocked', note: 'Enable to cause Stickers to be unlocked.', value: () => settings.forceStickersUnlocked },
{ type: 'switch', id: 'fakeInlineVencordEmotes', name: 'Fake Inline Hyperlink Emotes', note: 'Makes hyperlinked emojis appear as if they were real emojis, inlined in the message, similar to Vencord FakeNitro emotes.', value: () => settings.fakeInlineVencordEmotes },
{ type: 'switch', id: 'soundmojiEnabled', name: 'Soundmoji Bypass', note: 'Unlocks soundmojis and allows you to "send" them by automatically replacing them with a MP3 upload and some special text that will make them render as real soundmojis on the client side. Please note that this will enable Experiments.', value: () => settings.soundmojiEnabled }
]
},
{
type: 'category',
id: 'profile',
name: 'Profile',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'profileV2', name: 'Profile Accents', note: 'When enabled, you will see (almost) all users with the new Nitro-exclusive look for profiles (the sexier look). When disabled, the default behavior is used. Does not allow you to update your profile accent.', value: () => settings.profileV2 },
{ type: 'switch', id: 'fakeProfileThemes', name: 'Fake Profile Themes', note: 'Uses invisible 3y3 encoding to allow profile theming by hiding the colors in your bio.', value: () => settings.fakeProfileThemes },
{ type: 'switch', id: 'fakeProfileBanners', name: 'Fake Profile Banners', note: 'Uses invisible 3y3 encoding to allow setting profile banners by hiding the image URL in your bio. Only supports Imgur URLs for security reasons.', value: () => settings.fakeProfileBanners },
{ type: 'switch', id: 'userBgIntegration', name: 'UserBG Integration', note: 'Downloads and parses the UserBG JSON database so that UserBG banners will appear for you.', value: () => settings.userBgIntegration },
{ type: 'switch', id: 'fakeAvatarDecorations', name: 'Fake Avatar Decorations', note: 'Uses invisible 3y3 encoding to allow setting avatar decorations by hiding information in your bio and/or your custom status.', value: () => settings.fakeAvatarDecorations },
{ type: 'switch', id: 'profileEffects', name: 'Fake Profile Effects', note: 'Uses invisible 3y3 encoding to allow setting profile effects by hiding information in your bio.', value: () => settings.profileEffects },
{ type: 'switch', id: 'killProfileEffects', name: 'Kill Profile Effects', note: "Hate profile effects? Enable this and they'll be gone. All of them. Overrides all profile effects.", value: () => settings.killProfileEffects },
{ type: 'switch', id: 'customPFPs', name: 'Fake Profile Pictures', note: 'Uses invisible 3y3 encoding to allow setting custom profile pictures by hiding an image URL IN YOUR CUSTOM STATUS. Only supports Imgur URLs for security reasons.', value: () => settings.customPFPs },
{ type: 'switch', id: 'userPfpIntegration', name: 'UserPFP Integration', note: "Imports the UserPFP database so that people who have profile pictures in the UserPFP database will appear with their UserPFP profile picture. There's little reason to disable this.", value: () => settings.userPfpIntegration },
{ type: 'switch', id: 'disableUserBadge', name: 'Disable User Badge', note: 'Disables the BDNitro User Badge which appears on any user that uses Profile Customization. (client side)', value: () => settings.disableUserBadge },
{ type: 'switch', id: 'nameplatesEnabled', name: 'Fake Nameplates', note: 'Uses invisible 3y3 encoding to allow setting fake nameplates by hiding the information in your custom status and/or bio. Please paste the 3y3 in one or both of those areas.', value: () => settings.nameplatesEnabled }
]
},
{
type: 'category',
id: 'clips',
name: 'Clips',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'useClipBypass', name: 'Use Clips Bypass', note: 'Enabling this will effectively set your file upload limit for video files to 100MB. Disable this if you have a file upload limit larger than 100MB. Enabling this option will also enable Experiments.', value: () => settings.useClipBypass },
{ type: 'switch', id: 'alwaysTransmuxClips', name: 'Force Transmuxing', note: 'Always transmux the video, even if transmuxing would normally be skipped. Transmuxing is only ever skipped if the codec does not include AVC1 or includes MP42.', value: () => settings.alwaysTransmuxClips },
{ type: 'switch', id: 'forceClip', name: 'Force Clip', note: 'Always send video files as a clip, even if the size is below 10MB. I recommend that you leave this option disabled.', value: () => settings.forceClip },
{ type: 'switch', id: 'useAudioClipBypass', name: 'Audio Clips Bypass', note: 'Identical to the Clips Bypass for videos, except it works with audio files.', value: () => settings.useAudioClipBypass },
{ type: 'switch', id: 'forceAudioClip', name: 'Force Audio Clip', note: 'Always send audio files as a clip, even if the size is below 10MB. I recommend that you leave this option disabled.', value: () => settings.forceAudioClip },
{
type: 'switch',
id: 'zipClip',
name: 'ZipClip',
note: "Upload any file with the 100MB file upload limit by making your files into polyglot video+zip files that can be opened as a zip file. In 7-Zip, you will have to either: Rename the file to remove the .mp4 extension and then right-click and go 7-Zip > Open Archive > and then manually choose the file format (usually zip or 7z), or: Open the containing folder, right click the file and hit \"Open Inside\", then choose the zip. In WinRAR you don't need to do this, just rename if necessary, open, and it works. Windows' File Explorer's zip integration won't be able to open these, sorry. If you upload a file that is already an archive, the plugin will just append the file so the contents of your uploaded archive will appear rather than having your archive in a new zip.",
value: () => settings.zipClip
},
{ type: 'switch', id: 'enableClipsExperiment', name: 'Enable Clips Experiments', note: "Whether or not Clips-related experiments should be enabled. This doesn't disable on the fly, you will have to reload your client to get rid of the Experiments buttons in settings.", value: () => settings.enableClipsExperiment }
]
},
{
type: 'category',
id: 'miscellaneous',
name: 'Miscellaneous',
collapsible: true,
shown: false,
settings: [
{ type: 'switch', id: 'changePremiumType', name: 'Change PremiumType', note: "This is now optional. Enabling this may help compatibility for certain things or harm it. SimpleDiscordCrypt requires this to be enabled to have the emoji bypass work. Only enable this if you don't have Nitro.", value: () => settings.changePremiumType },
{ type: 'switch', id: 'clientThemes', name: 'Gradient Client Themes', note: 'Allows you to use Nitro-exclusive Client Themes.', value: () => settings.clientThemes },
{ type: 'switch', id: 'removeProfileUpsell', name: 'Remove Profile Customization Upsell', note: 'Removes the "Try It Out" upsell in the profile customization screen and replaces it with the Nitro variant. Note: does not allow you to use Nitro customization on Server Profiles as the API disallows this.', value: () => settings.removeProfileUpsell },
{ type: 'switch', id: 'removeScreenshareUpsell', name: 'Remove Screen Share Nitro Upsell', note: 'Removes the Nitro upsell in the Screen Share quality option menu.', value: () => settings.removeScreenshareUpsell },
{ type: 'switch', id: 'unlockAppIcons', name: 'App Icons', note: 'Unlocks app icons.', value: () => settings.unlockAppIcons },
{ type: 'switch', id: 'experiments', name: 'Experiments', note: 'Unlocks experiments. Use at your own risk.', value: () => settings.experiments },
{ type: 'switch', id: 'checkForUpdates', name: 'Check for Updates', note: 'Should the plugin check for updates on startup?', value: () => settings.checkForUpdates }
]
}
],
main: 'BDNitro.plugin.js'
};
// #endregion
// #region Exports
module.exports = class BDNitro {
constructor(meta) {
this.meta = meta;
}
getSettingsPanel() {
return UI.buildSettingsPanel({
settings: config.settingsPanel,
onChange: (category, id, value) => {
switch (id) {
case 'CustomResolution':
case 'CustomFPS':
settings[id] = parseInt(value);
this.saveAndUpdate();
break;
case 'minBitrate':
case 'targetBitrate':
case 'maxBitrate':
case 'voiceBitrate':
settings[id] = parseFloat(value);
this.saveAndUpdate();
break;
default:
settings[id] = value;
this.saveAndUpdate();
break;
}
}
});
}
// #region Save and Update
saveAndUpdate() {
//Saves and updates settings and runs functions
//migrate settings.avatarDecorations to data.avatarDecorations
if (settings.avatarDecorations) {
try {
data.avatarDecorations = settings.avatarDecorations;
this.saveDataFile();
delete settings.avatarDecorations;
} catch (err) {
Logger.error(this.meta.name, 'Data migration failed.');
}
}
//delete old nameplate data
if (data.nameplates) delete data.nameplates;
Data.save(this.meta.name, 'settings', settings);
this.saveDataFile();
Patcher.unpatchAll(this.meta.name);
Dispatcher.unsubscribe('COLLECTIBLES_CATEGORIES_FETCH_SUCCESS', this.storeProductsFromCategories);
if (settings.changePremiumType) {
try {
if (!(ORIGINAL_NITRO_STATUS > 1)) {
CurrentUser.premiumType = 1;
setTimeout(() => {
if (settings.changePremiumType) {
CurrentUser.premiumType = 1;
}
}, 10000);
}
} catch (err) {
Logger.error(this.meta.name, 'An error occurred changing premium type.' + err);
}
}
if (isNaN(settings.CustomFPS)) settings.CustomFPS = 60;
if (isNaN(settings.CustomResolution)) settings.CustomResolution = 1440;
if (settings.ResolutionSwapper) {
try {
this.resolutionSwapper();
this.resolutionSwapperV2();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.stickerBypass) {
try {
this.stickerSending();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.forceStickersUnlocked || settings.stickerBypass) {
try {
this.unlockStickers();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.emojiBypass) {
try {
this.emojiBypass();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.profileV2) {
try {
Patcher.after(this.meta.name, UserProfileStore, 'getUserProfile', (_, args, ret) => {
if (ret == undefined) return;
ret.premiumType = 2;
});
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.screenSharing) {
try {
this.customizeStreamButtons(); //Apply custom resolution and fps options for Go Live Modal V1
} catch (err) {
Logger.error(this.meta.name, 'Error occurred during customizeStreamButtons() ' + err);
}
try {
this.videoQualityModule(); //Custom Bitrates, FPS, Resolution
//disable resolution / fps check
Patcher.instead(this.meta.name, InvalidStreamSettingsModal, 'areStreamSettingsAllowed', (_, args, originalFunction) => {
return true;
});
} catch (err) {
Logger.error(this.meta.name, 'Error occurred during videoQualityModule() ' + err);
}
}
if (settings.clientThemes) {
try {
this.clientThemes();
} catch (err) {
Logger.warn(this.meta.name, err);
}
}
if (settings.fakeProfileThemes) {
try {
this.decodeAndApplyProfileColors();
this.encodeProfileColors();
} catch (err) {
Logger.error(this.meta.name, 'Error occurred running fakeProfileThemes bypass. ' + err);
}
}
DOM.removeStyle(this.meta.name);
if (settings.removeScreenshareUpsell) {
try {
DOM.addStyle(
this.meta.name,
`
[class*="upsellBanner"], [class*="reverseTrialEducationBannerContainer"] {
display: none;
visibility: hidden;
}
`
);
//Disable GoLiveModalV2 upsell
Patcher.instead(this.meta.name, GoLiveModalV2UpsellMod, 'GoLiveModalV2Upsell', () => {
return;
});
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.fakeProfileBanners) {
try {
this.bannerUrlDecoding();
this.bannerUrlEncoding(this.secondsightifyEncodeOnly);
} catch (err) {
Logger.error(this.meta.name, err);
}
}
Dispatcher.unsubscribe('COLLECTIBLES_CATEGORIES_FETCH_SUCCESS', this.storeProductsFromCategories);
if (settings.fakeAvatarDecorations) {
try {
this.fakeAvatarDecorations();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.unlockAppIcons) {
try {
this.appIcons();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.profileEffects) {
try {
this.profileFX(this.secondsightifyEncodeOnly);
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.killProfileEffects) {
try {
this.killProfileFX();
} catch (err) {
Logger.error(this.meta.name, 'Error occured during killProfileFX() ' + err);
}
}
DOM.removeStyle('BDNitroBadges');
try {
this.LoadingBadges();
} catch (err) {
Logger.error(this.meta.name, 'An error occurred during LoadingBadges() ' + err);
}
if (settings.customPFPs) {
try {
this.customProfilePictureDecoding();
this.customProfilePictureEncoding(this.secondsightifyEncodeOnly);
} catch (err) {
Logger.error(this.meta.name, 'An error occurred during customProfilePicture decoding/encoding. ' + err);
}
}
if (settings.experiments) {
try {
this.experiments();
} catch (err) {
Logger.error(this.meta.name, 'Error occurred in experiments() ' + err);
}
}
Patcher.instead(this.meta.name, canUserUseMod, 'canUserUse', (_, [feature, user], originalFunction) => {
if (settings.emojiBypass && (feature.name == 'emojisEverywhere' || feature.name == 'animatedEmojis')) return true;
if (settings.unlockAppIcons && feature.name == 'appIcons') return true;
if (settings.removeProfileUpsell && feature.name == 'profilePremiumFeatures') return true;
if (settings.clientThemes && feature.name == 'clientThemes') return true;
if (settings.soundmojiEnabled && feature.name == 'soundboardEverywhere') return true;
return originalFunction(feature, user);
});
//Clips Bypass
if (settings.useClipBypass || settings.useAudioClipBypass) {
try {
this.clipsBypass();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.fakeInlineVencordEmotes) {
try {
this.inlineFakemojiPatch();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.soundmojiEnabled || (settings.emojiBypass && settings.emojiBypassType == 0) || settings.stickerBypass) {
try {
this._sendMessageInsteadPatch();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.videoCodec2 > -1) {
try {
this.videoCodecs();
} catch (err) {
Logger.error(this.meta.name, err);
}
}
if (settings.fakeAvatarDecorations || settings.nameplatesEnabled) {
//subscribe to successful collectible category fetch event
Dispatcher.subscribe('COLLECTIBLES_CATEGORIES_FETCH_SUCCESS', this.storeProductsFromCategories);
//trigger collectibles fetch
FetchCollectibleCategories({
includeBundles: true,
includeUnpublished: false,
noCache: false,
paymentGateway: undefined
});
}
if (settings.nameplatesEnabled) {
this.nameplates();
}
} //End of saveAndUpdate()
// #endregion
//shouldInclude is a string containing the characters that the encoded text should contain
//that means that in order to check for "P{" for example, you check for the characters \uDB40\uDC50\uDB40\uDC7B since we're checking the encoded text
//but since the encoded text is over 2 bytes, you need to use the surrogate pairs ( you can calculate them here https://russellcottrell.com/greek/utilities/SurrogatePairCalculator.htm )
//if shouldInclude is blank, always return the revealed text if there is revealed text
getRevealedText(userId, shouldInclude = '') {
let revealedText = ''; //init variable
//get the user's profile from the cached user profiles
let userProfile = UserProfileStore.getUserProfile(userId);
//if this user's profile has been downloaded
if (userProfile) {
//if their bio is empty, move on to the next check.
if (userProfile?.bio != undefined) {
if (userProfile.bio.includes(shouldInclude)) {
//reveal 3y3 encoded text
revealedText = this.secondsightifyRevealOnly(String(userProfile.bio));
//if there's no 3y3 text, move on to the next check.
if (revealedText != undefined && revealedText != '') {
//return bio with the 3y3 decoded
return revealedText;
}
}
}
}
//get Custom Status
let customStatusActivity = PresenceStore.findActivity(userId, (e) => e.name == 'Custom Status' || e.id == 'custom');
//if the user has a custom status
if (customStatusActivity) {
//grab the text from the custom status
let customStatus = customStatusActivity.state;
//if something has gone horribly wrong, stop processing.
if (customStatus == undefined) return;
//reveal 3y3 encoded text
if (customStatus.includes(shouldInclude)) {
revealedText = this.secondsightifyRevealOnly(String(customStatus));
//return custom status with the 3y3 decoded
return revealedText;
}
}
}
//#region Nameplates
// nameplate 3y3 format: n{asset/palette}
nameplates() {
Patcher.after(this.meta.name, UserStore, 'getUser', (_, [userId], ret) => {
if (!ret || !userId) return;
let userNameplate = ret?.collectibles?.nameplate;
//if user has a nameplate
if (userNameplate) {
//filter out bad or existing nameplate
if (userNameplate.sku_id != 0 && userNameplate.sku_id != undefined && userNameplate.sku_id != null && data.nameplatesV2[userNameplate.skuId] == undefined) {
//get shortened asset name
let nameplateAsset = userNameplate.asset.replace('nameplates/', '').replaceAll('/', '');
//create name for nameplate since it's not provided through getUser
let nameplateName = nameplateAsset.replaceAll('_', ' '); //replace _ with space
nameplateName = nameplateName.replace(/(^\w|\s\w)/g, (m) => m.toUpperCase()); //make every word start with uppercase letter
//store seen nameplate
data.nameplatesV2[userNameplate.sku_id] = {
asset: userNameplate.asset.replace('nameplates/', ''),
palette: userNameplate.palette,
name: nameplateName
};
}
}
//Nameplate decoding
// check if it includes /n encoded
let revealedText = this.getRevealedText(userId, `\uDB40\uDC6E\uDB40\uDC7B`);
//if nothing's returned, or an empty string is returned, stop processing.
if (revealedText == undefined) return;
if (revealedText == '') return;
//This regex matches n{*} . (Do not fuck with this)
let regex = /n\{[^}]*?\}/;
//Check if there are any matches in the revealed text.
let matches = revealedText.match(regex);
if (matches == undefined) return;
let firstMatch = matches[0];
if (firstMatch == undefined) return;
//slice off the n{ and the ending }
let nameplate = firstMatch.slice(2, -1);
if (nameplate) {
let [asset, palette] = nameplate.split(',');
if (asset != undefined && palette != undefined) {
if (ret.collectibles == undefined) ret.collectibles = {};
ret.collectibles.nameplate = {
asset: `nameplates/${asset}`,
palette,
sku_id: 0
};
}
}
});
const secondsightifyEncodeOnly = this.secondsightifyEncodeOnly;
//#region Nameplates UI
function NameplateList() {
let [query, setQuery] = React.useState('');
let nameplatesList = [];
if (!data?.nameplatesV2 || data?.nameplatesV2?.length < 1) {
return React.createElement('h1', {
children: 'No nameplates were found!',
style: {
color: 'red',
fontWeight: 'bold'
}
});
} else {
const listOfNameplatesBySku = Object.keys(data.nameplatesV2);
for (let i = 0; i < listOfNameplatesBySku.length; i++) {
let sku = listOfNameplatesBySku[i];
let nameplate = data.nameplatesV2[sku];
if (query != '' && !nameplate.name.toLowerCase().includes(query.toLowerCase())) {
continue;
}
nameplatesList.push(
React.createElement('div', {
children: React.createElement(NameplatePreview, {
user: CurrentUser,
isHighlighted: true,
nameplateData: {
imgAlt: nameplate.name,
src: `nameplates/${nameplate.asset}`,
palette: NameplatePalettes[nameplate.palette]
}
}),
style: {
borderRadius: '10px',
width: '95%',
marginLeft: 'auto',
marginRight: 'auto',
height: '42px',
marginTop: '10px',
position: 'relative',
top: '5px',
cursor: 'pointer'
},
onClick: () => {
//make 3y3 string
let strToEncode = `n{${nameplate.asset},${nameplate.palette}}`;
let encodedStr = secondsightifyEncodeOnly(strToEncode);
//copy to clipboard
try {
DiscordNative.clipboard.copy(' ' + encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
},
title: nameplate.name
})
);
}
return React.createElement('div', {
children: [
React.createElement(Components.TextInput, {
value: query,
placeholder: 'Search...',
onChange: (input) => setQuery(input)
}),
React.createElement('br'),
React.createElement('div', {
children: nameplatesList
})
]
});
}
}
Patcher.after(this.meta.name, NameplateSectionMod, 'NameplateSection', (_, args, ret) => {
const ButtonsSection = ret.props.children.props.children;
ButtonsSection.push(
React.createElement('button', {
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
style: {
marginLeft: '10px',
whiteSpace: 'nowrap'
},
children: 'Change Nameplate [BDNitro]',
onClick: () => {
UI.showConfirmationModal('Change Nameplate', React.createElement(NameplateList), { cancelText: '' });
}
})
);
});
//#endregion
}
//#endregion
// #region Resolution Swapper
async resolutionSwapper() {
if (!this.StreamSettingsPanelMod) {
await Webpack.waitForModule(Webpack.Filters.byStrings('StreamSettings: user cannot be undefined'), { defaultExport: false });
this.StreamSettingsPanelMod = Webpack.getMangled('StreamSettings: user cannot be undefined', {
GoLiveModal: Webpack.Filters.byStrings('StreamSettings: user cannot be undefined')
});
}
if (!this.FormModalClasses) this.FormModalClasses = Webpack.getByKeys('formItemTitleSlim', 'modalContent');
Patcher.after(this.meta.name, this.StreamSettingsPanelMod, 'GoLiveModal', (_, [args], ret) => {
//Only if the selected preset is "Custom"
if (args.selectedPreset === 3) {
//Preparations
const childrenOfParentOfQualityButtonsSection = ret?.props?.children?.props?.children?.props?.children[1]?.props?.children;
const streamQualityButtonsSection = childrenOfParentOfQualityButtonsSection[0]?.props?.children;
const resolutionButtonsSection = streamQualityButtonsSection[0]?.props;
const fpsButtonsSection = streamQualityButtonsSection[1]?.props;
//Resolution input
if (resolutionButtonsSection?.children) {
//make each section into arrays so we can add another element
if (!Array.isArray(resolutionButtonsSection.children)) resolutionButtonsSection.children = [resolutionButtonsSection.children];
const thirdResolutionButton = resolutionButtonsSection?.children[0]?.props?.buttons[2];
resolutionButtonsSection?.children?.push(
React.createElement('div', {
children: [
React.createElement('h1', {
children: 'CUSTOM RESOLUTION',
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement(Components.NumberInput, {
value: settings.CustomResolution,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = 1440;
settings.CustomResolution = input;
//updates visual
thirdResolutionButton.value = input;
//sets values and saves to settings
this.customizeStreamButtons();
//simulate click on button -- serves to both select it and to make react re-render it.
thirdResolutionButton.onClick();
}
})
]
})
);
}
if (fpsButtonsSection?.children) {
fpsButtonsSection.children = [fpsButtonsSection.children];
const thirdFpsButton = fpsButtonsSection?.children[0]?.props?.buttons[2];
fpsButtonsSection?.children.push(
React.createElement('div', {
children: [
React.createElement('h1', {
children: 'CUSTOM FRAME RATE',
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement(Components.NumberInput, {
value: settings.CustomFPS,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = 60;
settings.CustomFPS = input;
//updates visual
thirdFpsButton.value = input;
//sets values and saves to settings
this.customizeStreamButtons();
//simulate click on button -- serves to both select it and to make react re-render it.
thirdFpsButton.onClick();
}
})
]
})
);
}
if (settings.CustomBitrateEnabled) {
if (childrenOfParentOfQualityButtonsSection) {
childrenOfParentOfQualityButtonsSection.push(React.createElement('br'));
childrenOfParentOfQualityButtonsSection.push(
React.createElement(Components.SettingGroup, {
name: 'Bitrate',
collapsible: true,
shown: false,
children: [
//headers
React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around'
},
children: [
React.createElement('h1', {
children: 'MIN',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement('h1', {
children: 'TARGET',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement('h1', {
children: 'MAX',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
})
]
}),
React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around',
marginBottom: '5px'
},
children: [
React.createElement(Components.NumberInput, {
value: settings.minBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
settings.minBitrate = input;
//save to settings
Data.save(this.meta.name, 'settings', settings);
}
}),
React.createElement(Components.NumberInput, {
value: settings.targetBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
settings.targetBitrate = input;
//save to settings
Data.save(this.meta.name, 'settings', settings);
}
}),
React.createElement(Components.NumberInput, {
value: settings.maxBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
settings.maxBitrate = input;
//save to settings
Data.save(this.meta.name, 'settings', settings);
}
})
]
})
]
})
);
}
}
}
});
}
//#region Go Live Modal V2
async resolutionSwapperV2() {
//wait for lazy loaded modules
await Webpack.waitForModule(Webpack.Filters.bySource('golivemodalv2'));
if (this.GoLiveModalMod == undefined)
this.GoLiveModalMod = Webpack.getMangled('golivemodalv2', {
goLiveModalV2: Webpack.Filters.byStrings('golivemodalv2')
});
await Webpack.waitForModule(Webpack.Filters.byKeys('streamOptionsButton', 'settingsIcon'));
if (this.SteamOptionsButtonClassesMod == undefined) this.SteamOptionsButtonClassesMod = Webpack.getByKeys('streamOptionsButton', 'settingsIcon');
//the sign of janky code inbound
let GLMV2Opt = {
resolutionToSet: undefined,
fpsToSet: undefined,
minBitrateToSet: undefined,
targetBitrateToSet: undefined,
maxBitrateToSet: undefined
};
Patcher.after(this.meta.name, this.GoLiveModalMod, 'goLiveModalV2', (_, args, ret) => {
//maybe the worst amalgamation in this whole plugin?
if (GLMV2Opt.resolutionToSet != undefined) {
ret.props.state.resolution = GLMV2Opt.resolutionToSet;
settings.CustomResolution = GLMV2Opt.resolutionToSet;
GLMV2Opt.resolutionToSet = undefined;
}
if (GLMV2Opt.fpsToSet != undefined) {
ret.props.state.fps = GLMV2Opt.fpsToSet;
settings.CustomFPS = GLMV2Opt.fpsToSet;
GLMV2Opt.fpsToSet = undefined;
}
const ModalFooter = ret?.props?.children?.props?.children[2]?.props?.children[0]?.props?.children[1]?.props?.children;
if (ModalFooter) {
ModalFooter.splice(
2,
0,
React.createElement('button', {
class: `${this.SteamOptionsButtonClassesMod.streamOptionsButton} ${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorPrimary} ${buttonClassModule.sizeIcon} ${buttonClassModule.grow}`,
style: {
height: '46px',
width: '46px'
},
children: 'YABD',
onClick: () => {
let localStreamOptions = {
resolutionToSet: undefined,
fpsToSet: undefined,
minBitrateToSet: undefined,
targetBitrateToSet: undefined,
maxBitrateToSet: undefined
};
//defaults
if (settings.ResolutionEnabled) localStreamOptions.resolutionToSet = settings.CustomResolution;
if (settings.CustomFPSEnabled) localStreamOptions.fpsToSet = settings.CustomFPS;
if (settings.CustomBitrateEnabled) {
localStreamOptions.minBitrateToSet = settings.minBitrate;
localStreamOptions.targetBitrateToSet = settings.targetBitrate;
localStreamOptions.maxBitrateToSet = settings.maxBitrate;
}
UI.showConfirmationModal(
'Configure Stream Settings',
[
React.createElement('div', {
children: [
React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around'
},
children: [
React.createElement('h1', {
children: 'Resolution',
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement('h1', {
children: 'FPS',
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
})
]
}),
React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around'
},
children: [
React.createElement(Components.NumberInput, {
value: settings.CustomResolution,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = 1440;
localStreamOptions.resolutionToSet = input;
}
}),
React.createElement(Components.NumberInput, {
value: settings.CustomFPS,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = 60;
localStreamOptions.fpsToSet = input;
}
})
]
})
]
}),
settings.CustomBitrateEnabled ? React.createElement('br') : undefined,
settings.CustomBitrateEnabled
? React.createElement('h1', {
children: 'Custom Bitrate (kbps)',
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
})
: undefined,
settings.CustomBitrateEnabled
? React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around'
},
children: [
React.createElement('h1', {
children: 'Min',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement('h1', {
children: 'Target',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
}),
React.createElement('h1', {
children: 'Max',
style: {
marginBlock: '0 5px'
},
className: `${TextClasses.h5} ${TextClasses.eyebrow} ${this.FormModalClasses.formItemTitleSlim}`
})
]
})
: undefined,
React.createElement('div', {
style: {
display: 'flex',
width: '100%',
justifyContent: 'space-around',
marginBottom: '5px'
},
children: settings.CustomBitrateEnabled
? [
React.createElement(Components.NumberInput, {
value: settings.minBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
localStreamOptions.minBitrateToSet = input;
}
}),
React.createElement(Components.NumberInput, {
value: settings.targetBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
localStreamOptions.targetBitrateToSet = input;
}
}),
React.createElement(Components.NumberInput, {
value: settings.maxBitrate,
min: -1,
onChange: (input) => {
input = parseInt(input);
if (isNaN(input)) input = -1;
localStreamOptions.maxBitrateToSet = input;
}
})
]
: undefined
})
],
{
confirmText: 'Apply',
onConfirm: () => {
GLMV2Opt = localStreamOptions;
if (localStreamOptions.minBitrateToSet != undefined) settings.minBitrate = localStreamOptions.minBitrateToSet;
if (localStreamOptions.targetBitrateToSet != undefined) settings.targetBitrate = localStreamOptions.targetBitrateToSet;
if (localStreamOptions.maxBitrateToSet != undefined) settings.maxBitrate = localStreamOptions.maxBitrateToSet;
Data.save(this.meta.name, 'settings', settings);
}
}
);
}
})
);
}
});
}
// #endregion
unlockStickers() {
Patcher.instead(this.meta.name, stickerSendabilityModule, 'getStickerSendability', () => {
return 0; //SENDABLE
});
Patcher.instead(this.meta.name, stickerSendabilityModule, 'isSendableSticker', () => {
return true;
});
}
videoCodecs() {
Patcher.after(this.meta.name, streamSettingsMod, 'getCodecOptions', (_, args, ret) => {
ret.videoEncoder = ret.videoDecoders[settings.videoCodec2];
});
}
// #region Clips Bypasses
async clipsBypass() {
if (settings.enableClipsExperiment) {
this.experiments();
this.overrideExperiment('2023-09_clips_nitro_early_access', 2);
this.overrideExperiment('2022-11_clips_experiment', 1);
this.overrideExperiment('2023-10_viewer_clipping', 1);
}
//spoof nitro file size limit
Patcher.instead(this.meta.name, MaxFileSizeMod, 'getMaxFileSize', (_, args) => {
return 500 * 1024 * 1024; //512 MB
});
//disable max file size message
Patcher.instead(this.meta.name, MaxFileSizeMod, 'exceedsMessageSizeLimit', (_, args) => {
return false;
});
// todo: maybe fix ActionBarClipsButton and ClipsButton button not appearing with experiments disabled eventually
// currently they use useExperiment to check if they should appear, which is a function that I can't patch
// and remaking the respective React elements sounds really difficult
//base64 for file clipping mp4
const clipMe =
'AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAABbBtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAyAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAACUXRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAMgAAADIAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAMgAAAAAAAEAAAAAAcltZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAADIAAAAKAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAF0bWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABNHN0YmwAAADAc3RzZAAAAAAAAAABAAAAsG1wNHYAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAMgAyAEgAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAsZXNkcwAAAAADgICAGwABAASAgIANbBEAAAAAAMmQAADJkAaAgIABAgAAAApmaWVsAQAAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAADJkAAAyZAAAAAYc3R0cwAAAAAAAAABAAAABQAAAgAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAAEAAAABAAAAFHN0c3oAAAAAAAABAgAAAAUAAAAkc3RjbwAAAAAAAAAFAAAF8QAABvsAAAgFAAAJDwAAChUAAAKJdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAgAAAAAAAAC6AAAAAAAAAAAAAAABAQAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAAuQAABAAAAQAAAAACAW1kaWEAAAAgbWRoZAAAAAAAAAAAAAAAAAAArEQAACPfVcQAAAAAAC1oZGxyAAAAAAAAAABzb3VuAAAAAAAAAAAAAAAAU291bmRIYW5kbGVyAAAAAaxtaW5mAAAAEHNtaGQAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAXBzdGJsAAAAfnN0c2QAAAAAAAAAAQAAAG5tcDRhAAAAAAAAAAEAAAAAAAAAAAACABAAAAAArEQAAAAAADZlc2RzAAAAAAOAgIAlAAIABICAgBdAFQAAAAAAB/QAAAf0BYCAgAUSCFblAAaAgIABAgAAABRidHJ0AAAAAAAAB/QAAAf0AAAAIHN0dHMAAAAAAAAAAgAAAAgAAAQAAAAAAQAAA98AAAA0c3RzYwAAAAAAAAADAAAAAQAAAAEAAAABAAAAAgAAAAIAAAABAAAABQAAAAEAAAABAAAAOHN0c3oAAAAAAAAAAAAAAAkAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAoc3RjbwAAAAAAAAAGAAAF3AAABvMAAAf9AAAJBwAAChEAAAsXAAAAGnNncGQBAAAAcm9sbAAAAAIAAAAB//8AAAAcc2JncAAAAAByb2xsAAAAAQAAAAkAAAABAAAAYnVkdGEAAABabWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAtaWxzdAAAACWpdG9vAAAAHWRhdGEAAAABAAAAAExhdmY1OS4yNy4xMDAAAAAIZnJlZQAABUdtZGF03gIATGF2YzU5LjM3LjEwMAACMEAO/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzU5LjM3LjEwMAD//gAMQ1M9SVRVNjAxAP/bAEMACAQEBAQEBQUFBQUFBgYGBgYGBgYGBgYGBgcHBwgICAcHBwYGBwcICAgICQkJCAgICAkJCgoKDAwLCw4ODhERFP/EAEsAAQEAAAAAAAAAAAAAAAAAAAAHAQEAAAAAAAAAAAAAAAAAAAAAEAEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/8AAEQgAMgAyAwEiAAIRAAMRAP/aAAwDAQACEQMRAD8Ah4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZARggBwEYIAf/2P/gABBKRklGAAECAAABAAEAAP/+ABBMYXZjNTkuMzcuMTAwAP/+AAxDUz1JVFU2MDEA/9sAQwAIBAQEBAQFBQUFBQUGBgYGBgYGBgYGBgYGBwcHCAgIBwcHBgYHBwgICAgJCQkICAgICQkKCgoMDAsLDg4OEREU/8QASwABAQAAAAAAAAAAAAAAAAAAAAcBAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAyADIDASIAAhEAAxEA/9oADAMBAAIRAxEAPwCHgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9kBGCAHARggB//Y/+AAEEpGSUYAAQIAAAEAAQAA//4AEExhdmM1OS4zNy4xMDAA//4ADENTPUlUVTYwMQD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABLAAEBAAAAAAAAAAAAAAAAAAAABwEBAAAAAAAAAAAAAAAAAAAAABABAAAAAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/AABEIADIAMgMBIgACEQADEQD/2gAMAwEAAhEDEQA/AIeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/2QEYIAcBGCAH/9j/4AAQSkZJRgABAgAAAQABAAD//gAQTGF2YzU5LjM3LjEwMAD//gAMQ1M9SVRVNjAxAP/bAEMACAQEBAQEBQUFBQUFBgYGBgYGBgYGBgYGBgcHBwgICAcHBwYGBwcICAgICQkJCAgICAkJCgoKDAwLCw4ODhERFP/EAEsAAQEAAAAAAAAAAAAAAAAAAAAHAQEAAAAAAAAAAAAAAAAAAAAAEAEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/8AAEQgAMgAyAwEiAAIRAAMRAP/aAAwDAQACEQMRAD8Ah4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZARggB//Y/+AAEEpGSUYAAQIAAAEAAQAA//4AEExhdmM1OS4zNy4xMDAA//4ADENTPUlUVTYwMQD/2wBDAAgEBAQEBAUFBQUFBQYGBgYGBgYGBgYGBgYHBwcICAgHBwcGBgcHCAgICAkJCQgICAgJCQoKCgwMCwsODg4RERT/xABLAAEBAAAAAAAAAAAAAAAAAAAABwEBAAAAAAAAAAAAAAAAAAAAABABAAAAAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/AABEIADIAMgMBIgACEQADEQD/2gAMAwEAAhEDEQA/AIeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/2QEYIAcAAABZbWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAsaWxzdAAAACSpdG9vAAAAHGRhdGEAAAABAAAAAExhdmY2MS4zLjEwMwAALi51dWlkochSmTNGTbiI8IP1enWl7w==';
//convert base64 to ArrayBuffer
var binaryString = atob(clipMe);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
const clipMaBuffer = bytes.buffer;
if (!this.MP4Box) {
try {
await Webpack.getByStrings('mp4boxInputFile.boxes')();
} catch (e) {}
this.MP4Box = await Webpack.waitForModule(Webpack.Filters.byKeys('MP4BoxStream'));
}
if (ffmpeg == undefined) await this.loadFFmpeg();
async function ffmpegTransmux(arrayBuffer, inFileName = 'input.mp4', ffmpegArguments, outFileName = 'output.mp4') {
if (ffmpeg) {
if (!ffmpegArguments) ffmpegArguments = ['-i', inFileName, '-codec', 'copy', '-brand', 'isom/avc1', '-movflags', '+faststart', '-map', '0', '-map_metadata', '-1', '-map_chapters', '-1', outFileName];
await ffmpeg.writeFile(inFileName, new Uint8Array(arrayBuffer));
console.log('Approximately equivalent ffmpeg command:');
console.log('ffmpeg ' + ffmpegArguments.join(' '));
await ffmpeg.exec(ffmpegArguments);
const data = await ffmpeg.readFile(outFileName);
ffmpeg.deleteFile(inFileName);
ffmpeg.deleteFile(outFileName);
if (data.length == 0) {
throw new Error(`An error occurred during muxing/encoding: Output file ended up empty or doesn't exist,
likely due to an FFmpeg error. Please check the FFmpeg logs above. If you need assistance,
please use the support channel in the Discord server.`);
}
return data.buffer;
} else throw new Error(`Can't mux/encode: ffmpeg is not loaded!`);
}
async function ffmpegAudioTransmux(arrayBuffer, inFileName = 'input.mp3', outFileName = 'output.mp4') {
let ffmpegArgs = ['-f', 'lavfi', '-i', 'color=c=black:s=400x50', '-i', inFileName, '-shortest', '-fflags', '+shortest', '-brand', 'isom/avc1', '-movflags', '+faststart', '-map_metadata', '-1', '-map_chapters', '-1', '-preset', 'ultrafast', '-c:a', 'copy', '-strict', '-2', '-tune', 'stillimage', '-r', '1', outFileName];
return await ffmpegTransmux(arrayBuffer, inFileName, ffmpegArgs, outFileName);
}
const skippedAudioTypes = ['audio/mid', 'audio/basic', 'audio/mpegurl', 'audio/3gp'];
const skippedVideoTypes = ['video/3gp', 'video/asf', 'video/ivf'];
Patcher.instead(this.meta.name, addFilesMod, 'addFiles', async (_, [args], originalFunction) => {
/* If ffmpeg isn't loaded, or was unloaded for some reason,
when the user adds a file, make sure to load it again if it's undefined
If we don't do this check, then the user would have to
trigger saveAndUpdate or restart the plugin to
make ffmpeg load if it wasn't loaded properly the first time. */
if (ffmpeg == undefined) await this.loadFFmpeg();
function errorHandler(err, currentFile, name) {
UI.showToast('Something went wrong. See console for details.', { type: 'error', forceShow: true });
Logger.error(name, err);
if (currentFile) {
Logger.info(name, 'Current file information for debugging:');
Logger.info(name, currentFile);
Logger.info(name, `File Type: "${currentFile.file?.type}"`);
}
}
//for each file being added
for (let i = 0; i < args.files.length; i++) {
const currentFile = args.files[i];
if (currentFile.file.name.endsWith('.dlfc')) return;
const clipData = {
id: '',
version: 3,
applicationName: '',
applicationId: '1301689862256066560',
users: [CurrentUser.id],
clipMethod: 'manual',
length: currentFile.file.size,
thumbnail: '',
filepath: '',
name: currentFile.file.name.substring(0, currentFile.file.name.lastIndexOf('.'))
};
// #region MP4 Clip
//larger than 10mb or force video clip enabled AND video clip bypass enabled AND is a video file AND is not a video type to skip
if ((currentFile.file.size > 10485759 || settings.forceClip) && settings.useClipBypass && currentFile.file.type.startsWith('video/') && !skippedVideoTypes.includes(currentFile.file.type)) {
//if this file is an mp4 file
if (currentFile.file.type == 'video/mp4') {
let dontStopMeNow = true;
let mp4BoxFile = this.MP4Box.createFile();
mp4BoxFile.onError = (e) => {
Logger.error(this.meta.name, e);
dontStopMeNow = false;
};
mp4BoxFile.onReady = async (info) => {
mp4BoxFile.flush();
try {
//check if file is H264 or H265
if (info.videoTracks[0]?.codec?.startsWith('avc') || info.videoTracks[0]?.codec?.startsWith('hev1')) {
let hasTransmuxed = false;
if (!info.brands.includes('avc1') || info.brands.includes('mp42') || settings.alwaysTransmuxClips) {
arrayBuffer = await ffmpegTransmux(arrayBuffer, currentFile.file.name);
hasTransmuxed = true;
}
let isMetadataPresent = false;
//skip if we transmuxed since we know it won't have the tag
if (!hasTransmuxed) {
//Is this file already a Discord clip?
for (let j = 0; j < mp4BoxFile.boxes.length; j++) {
if (mp4BoxFile.boxes[j].type == 'uuid') {
isMetadataPresent = true;
}
}
}
//If this file is not a Discord clip, append udtaBuffer
if (!isMetadataPresent) {
let array1 = ArrayBuffer.concat(arrayBuffer, udtaBuffer);
let video = new File([new Uint8Array(array1)], currentFile.file.name, { type: 'video/mp4' });
currentFile.file = video;
}
} else {
//file is not H264 or H265, but is an mp4
arrayBuffer = await ffmpegTransmux(arrayBuffer, currentFile.file.name);
let array1 = ArrayBuffer.concat(arrayBuffer, udtaBuffer);
let video = new File([new Uint8Array(array1)], currentFile.file.name, { type: 'video/mp4' });
currentFile.file = video;
}
//send as a "clip"
currentFile.clip = clipData;
} catch (err) {
errorHandler(err, currentFile, this.meta.name);
} finally {
dontStopMeNow = false;
}
};
let arrayBuffer;
currentFile.file.arrayBuffer().then((obj) => {
arrayBuffer = obj;
arrayBuffer.fileStart = 0;
//examine file with mp4Box.
mp4BoxFile.appendBuffer(arrayBuffer);
//onReady will run after the buffer is appended successfully
});
//wait for onReady to finish
while (dontStopMeNow) {
await new Promise((r) => setTimeout(r, 10));
}
}
// #endregion
// #region Other Video Clip
else if (currentFile.file.name.toLowerCase().endsWith('.mod') && currentFile.file.type == 'video/mpeg') {
continue;
} else {
//Is a video file, but not MP4
let outFileName = 'output.mp4';
//AVI file warning
if (currentFile.file.type == 'video/avi') {
UI.showToast('[BDNitro] NOTE: AVI Files may send, but HTML5 and MP4 do not support all AVI video codecs, it may not play and FFmpeg may error!', { type: 'warning' });
}
try {
let arrayBuffer = await currentFile.file.arrayBuffer();
const movTypes = ['video/flv', 'video/ogg', 'video/wmv', 'video/mov'];
if (movTypes.includes(currentFile.file.type)) {
Logger.info(this.meta.name, 'Using MOV format for clip.');
outFileName = 'output.mov';
}
let array1 = ArrayBuffer.concat(await ffmpegTransmux(arrayBuffer, currentFile.file.name, undefined, outFileName), udtaBuffer);
let video = new File([new Uint8Array(array1)], currentFile.file.name.substr(0, currentFile.file.name.lastIndexOf('.')) + '.mp4', { type: 'video/mp4' });
currentFile.file = video;
//send as a "clip"
currentFile.clip = clipData;
} catch (err) {
errorHandler(err, currentFile, this.meta.name);
continue;
}
}
//#endregion
}
// #region Audio Clip
//Audio file above 10mb or Force Audio Clip and it not an incompatible type and useAudioClipBypass is true
else if (settings.useAudioClipBypass && (currentFile.file.size > 10485759 || settings.forceAudioClip) && currentFile.file.type.startsWith('audio/') && !skippedAudioTypes.includes(currentFile.file.type)) {
try {
let arrayBuffer = await currentFile.file.arrayBuffer();
let outFileName = 'output.mp4';
if (['audio/wav', 'audio/aiff', 'audio/x-ms-wma'].includes(currentFile.file.type)) {
Logger.info('BDNitro', 'Using MOV format for audio clip.');
outFileName = 'output.mov';
}
if (currentFile.file.type == 'audio/vnd.dolby.dd-raw') {
UI.showToast('AC3 should send but playback is not supported!', { type: 'warn' });
}
let array1 = ArrayBuffer.concat(await ffmpegAudioTransmux(arrayBuffer, currentFile.file.name, outFileName), udtaBuffer);
let video = new File([new Uint8Array(array1)], clipData.name + '.mp4', { type: 'video/mp4' });
currentFile.file = video;
//send as a "clip"
currentFile.clip = clipData;
} catch (err) {
errorHandler(err, currentFile, this.meta.name);
continue;
}
}
//#endregion
// #region File Clip
//any file above 10mb and below 100mb that does not fit any previous criteria
else if (currentFile.file.size > 10485759 && currentFile.file.size < 104857590 && settings.zipClip) {
const archiveMimeTypes = ['x-7z-compressed', 'x-bzip', 'x-bzip2', 'x-rar-compressed', 'x-tar', 'gzip', 'x-gzip', 'zip', 'x-zip-compressed'];
let zipFile;
let fileArrayBuffer = await currentFile.file.arrayBuffer();
//if the file has an archive mime type or is a .001 through .999 part file. technically also would work with more than 999 parts but i dont think it goes that high lol
if (archiveMimeTypes.includes(currentFile.file.type.replace('application/', '')) || parseInt(currentFile.file.name.substring(currentFile.file.name.lastIndexOf('.') + 1, currentFile.file.name.length)) > 0) {
zipFile = fileArrayBuffer;
clipData.name = currentFile.file.name;
} else {
/* DeepSeek-R1 helped to write this createZip function.
Don't worry, I'm not completely stupid, I understand what the code does, how it works, and made sure to optimize it.
I was just not feeling like learning the ins and outs of the zip format totally from scratch. Sue me.
An explanation of the function is below (yes I wrote the explanation):
The function creates a basic zip file containing the data variable as a file with no compression and returns a Uint8Array of the zip file.
The name variable is the file name of the file within the zip.
The data variable can be ArrayBuffer, Uint8Array, or string.
To make a zip file, a bunch of headers and data descriptors, including a CRC checksum and a bunch of info about the file, must be created, so that's what we're doing.
https://en.wikipedia.org/wiki/ZIP_(file_format)#File_headers for more information on that.
Writing all this shit would've been pretty tedious so yea. */
function createZip(name, data) {
// Convert input to Uint8Array
const enc = new TextEncoder();
const nameBytes = enc.encode(name);
const dataBytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data instanceof Uint8Array ? data : enc.encode(data);
// Calculate CRC and lengths
let crc = -1; // Initial value
const len = dataBytes.length;
// Process bytes in chunks
for (let i = 0; i < len; i++) {
crc = (crc >>> 8) ^ crcTable[(crc ^ dataBytes[i]) & 0xff];
}
// Finalize CRC and convert to unsigned int
crc = (crc ^ -1) >>> 0;
const dataLength = dataBytes.length;
const headerLength = 30 + nameBytes.length;
// Local File Header (starts at 0)
const localHeader = new DataView(new ArrayBuffer(headerLength));
localHeader.setUint32(0, 0x04034b50, true); // Signature
localHeader.setUint16(4, 0x0a00, true); // Version needed
localHeader.setUint32(14, crc, true); // CRC-32
localHeader.setUint32(18, dataLength, true); // Compressed size
localHeader.setUint32(22, dataLength, true); // Uncompressed size
localHeader.setUint16(26, nameBytes.length, true);
new Uint8Array(localHeader.buffer).set(nameBytes, 30);
// Central Directory (starts after file data)
// Note: Omitted fields default to 0, since the length is set manually.
const centralDir = new DataView(new ArrayBuffer(46 + nameBytes.length));
centralDir.setUint32(0, 0x02014b50, true); // Signature
centralDir.setUint16(6, 0x0a00, true); // Version needed
centralDir.setUint32(16, crc, true); // CRC-32
centralDir.setUint32(20, dataLength, true); // Sizes
centralDir.setUint32(24, dataLength, true);
centralDir.setUint16(28, nameBytes.length, true);
new Uint8Array(centralDir.buffer).set(nameBytes, 46);
// End of Central Directory
const end = new DataView(new ArrayBuffer(22));
end.setUint32(0, 0x06054b50, true); // Signature
end.setUint16(8, 1, true); // Entry count
end.setUint16(10, 1, true); // Total entries
end.setUint32(12, centralDir.buffer.byteLength, true); // Dir size
end.setUint32(16, headerLength + dataLength, true); // Dir offset
//Allocating a Uint8Array large enough for the file
const totalSize = localHeader.buffer.byteLength + dataBytes.length + centralDir.buffer.byteLength + end.buffer.byteLength;
const result = new Uint8Array(totalSize);
//Putting all the data together
let offset = 0;
[localHeader.buffer, dataBytes, centralDir.buffer, end.buffer].forEach((buf) => {
result.set(new Uint8Array(buf), offset);
offset += buf.byteLength || buf.length;
});
return result;
}
zipFile = createZip(currentFile.file.name, fileArrayBuffer).buffer;
clipData.name += '.zip';
}
try {
let newArrBuf = ArrayBuffer.concat(clipMaBuffer, zipFile);
let newFile = new File([new Uint8Array(newArrBuf)], clipData.name + '.mp4', { type: 'video/mp4' });
currentFile.file = newFile;
currentFile.clip = clipData;
} catch (err) {
errorHandler(err, currentFile, this.meta.name);
}
}
//#endregion
currentFile.platform = 1;
}
originalFunction(args);
});
Patcher.after(this.meta.name, ClipsEnabledMod, 'useEnableClips', (_, args, ret) => {
//I have no earthly idea why but, instead patching this one causes React crashes./
// Luckily after-patching prevents it from crashing and it still unlocks it as it should
return true;
});
Patcher.instead(this.meta.name, ClipsEnabledMod, 'areClipsEnabled', () => {
return true;
});
Patcher.instead(this.meta.name, ClipsEnabledMod, 'isPremium', () => {
return true;
});
Patcher.instead(this.meta.name, ClipsAllowedMod, 'isClipsClientCapable', () => {
return true;
});
Patcher.instead(this.meta.name, ClipsStore, 'isViewerClippingAllowedForUser', () => {
return true;
});
Patcher.instead(this.meta.name, ClipsStore, 'isClipsEnabledForUser', () => {
return true;
});
Patcher.instead(this.meta.name, ClipsStore, 'isVoiceRecordingAllowedForUser', () => {
return true;
});
} //End of clipsBypass()
// #endregion
// #region Load FFmpeg.js
async loadFFmpeg() {
const defineTemp = window.global.define;
let ffmpegScript = document.getElementById('ffmpegScript');
if (ffmpegScript) {
ffmpegScript.remove();
}
async function fetchAndRetryWithNetFetch(filename) {
const ffmpeg_js_baseurl = 'https://raw.githubusercontent.com/srgobi/BDNitro/refs/heads/main/ffmpeg/';
let res = await fetch(ffmpeg_js_baseurl + filename, { timeout: 100000, cache: 'force-cache' });
if (res.ok || res.status == 200) {
return res;
} else {
Logger.warn('BDNitro', res);
res = await Net.fetch(ffmpeg_js_baseurl + filename, { timeout: 100000 });
if (res.ok || res.status == 200) {
return res;
} else {
Logger.error('BDNitro', res);
throw new Error(filename + ' failed to fetch.');
}
}
}
try {
//load 814.ffmpeg.js (ffmpeg worker)
let ffmpegWorkerURL = URL.createObjectURL(await (await fetchAndRetryWithNetFetch('814.ffmpeg.js')).blob());
//load FFmpeg.js as text
let ffmpegSrc = await (await fetchAndRetryWithNetFetch('ffmpeg.js')).text();
//patch worker URL in the source of ffmpeg.js (why is this a problem lmao)
ffmpegSrc = ffmpegSrc.replace(`new URL(e.p+e.u(814),e.b)`, `"${ffmpegWorkerURL.toString()}"`);
//blob ffmpeg
const ffmpegURL = URL.createObjectURL(new Blob([ffmpegSrc]));
// for some reason, for ffmpeg.js to work we need to set global define to undefined temporarily.
// since for a brief moment it is undefined, any function that uses it may throw an error during that window.
window.global.define = undefined;
//load external JS as a script
await new Promise((load, err) => {
const ffmpegScriptElem = document.getElementById('ffmpegScript') || document.createElement('script');
ffmpegScriptElem.id = 'ffmpegScript';
ffmpegScriptElem.src = ffmpegURL;
ffmpegScriptElem.onload = load;
ffmpegScriptElem.onerror = err;
document.head.appendChild(ffmpegScriptElem);
});
window.global.define = defineTemp;
//load ffmpeg core
let ffmpegCoreURL = URL.createObjectURL(await (await fetchAndRetryWithNetFetch('ffmpeg-core.js')).blob());
let ffmpegCoreWasmURL = URL.createObjectURL(await (await fetchAndRetryWithNetFetch('ffmpeg-core.wasm')).blob());
if (FFmpegWASM && ffmpegCoreURL && ffmpegCoreWasmURL && ffmpegWorkerURL) {
ffmpeg = new FFmpegWASM.FFmpeg();
await ffmpeg.load({
coreURL: ffmpegCoreURL,
wasmURL: ffmpegCoreWasmURL
});
Logger.info(this.meta.name, 'FFmpeg load success!');
ffmpeg.on('log', ({ message }) => {
console.log(message);
});
} else {
Logger.info(this.meta.name, FFmpegWASM);
Logger.info(this.meta.name, ffmpegCoreURL);
Logger.info(this.meta.name, ffmpegCoreWasmURL);
Logger.info(this.meta.name, ffmpegWorkerURL);
throw new Error('One or more of the necessary components failed to load.');
}
} catch (err) {
UI.showToast('An error occured trying to load FFmpeg.wasm. Check console for details.', { type: 'error', forceShow: true });
Logger.info(this.meta.name, 'FFmpeg failed to load. The clips bypass will not work without this unless the file is already the correct format! Include above and below error messages when reporting!');
Logger.error(this.meta.name, err);
} finally {
//Ensure we return window.global.define to its regular state just in case we errored during the short window where it has to be set to undefined.
window.global.define = defineTemp;
}
} //End of loadFFmpeg()
// #endregion
// #region Experiments
async experiments() {
try {
//code heavily modified from https://gist.github.com/JohannesMP/afdf27383608c3b6f20a6a072d0be93c?permalink_comment_id=4784940#gistcomment-4784940
CurrentUser.flags |= 1;
const Stores = Object.values(UserStore._dispatcher._actionHandlers._dependencyGraph.nodes);
Stores.find((x) => x.name === 'DeveloperExperimentStore').actionHandler['CONNECTION_OPEN']();
try {
Stores.find((x) => x.name === 'ExperimentStore').actionHandler['OVERLAY_INITIALIZE']({ user: { flags: 1 } });
} catch {}
Stores.find((x) => x.name === 'ExperimentStore').storeDidChange();
} catch (err) {
Logger.warn(this.meta.name, err);
}
}
overrideExperiment(type, bucket) {
//console.log("applying experiment override " + type + "; bucket " + bucket);
Dispatcher.dispatch({
type: 'EXPERIMENT_OVERRIDE_BUCKET',
experimentId: type,
experimentBucket: bucket
});
}
// #endregion
// #region Client Themes
clientThemes() {
//delete isPreview property so that we can set our own
delete clientThemesModule.isPreview;
//this property basically unlocks the client theme buttons
Object.defineProperty(clientThemesModule, 'isPreview', {
//Enabling the nitro theme settings
value: false,
configurable: true,
enumerable: true,
writable: true
});
//Patching saveClientTheme function.
Patcher.instead(this.meta.name, themesModule, 'saveClientTheme', (_, [args]) => {
//if user is trying to set the theme to a default theme
if (args.backgroundGradientPresetId == undefined) {
//If this number is -1, that indicates to the plugin that the current theme we're setting to is not a gradient nitro theme.
settings.lastGradientSettingStore = -1;
//save any changes to settings
Data.save(this.meta.name, 'settings', this.settings);
//dispatch settings update to change themes
Dispatcher.dispatch({
type: 'SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE',
changes: {
appearance: {
shouldSync: false, //prevent sync to stop discord api from butting in. Since this is not a nitro theme, shouldn't this be set to true? Idk, but I'm not touching it lol.
settings: {
theme: args.theme,
developerMode: true //genuinely have no idea what this does.
}
}
}
});
return;
} else {
//gradient themes
//Store the last gradient setting used in settings
settings.lastGradientSettingStore = args.backgroundGradientPresetId;
//save any changes to settings
Data.save(this.meta.name, 'settings', this.settings);
//dispatch settings update event to change to the gradient the user chose
Dispatcher.dispatch({
type: 'SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE',
changes: {
appearance: {
shouldSync: false, //prevent sync to stop discord api from butting in
settings: {
theme: args.theme, //gradient themes are based off of either dark or light, args.theme stores this information
clientThemeSettings: {
backgroundGradientPresetId: args.backgroundGradientPresetId //preset ID for the gradient theme
},
developerMode: true
}
}
}
});
//update background gradient preset to the one that was just chosen.
Dispatcher.dispatch({
type: 'UPDATE_BACKGROUND_GRADIENT_PRESET',
presetId: settings.lastGradientSettingStore
});
}
}); //End of saveClientTheme patch.
//If last appearance choice was a nitro client theme
if (settings.lastGradientSettingStore != -1) {
//This sets the gradient on plugin save and load.
Dispatcher.dispatch({
type: 'UPDATE_BACKGROUND_GRADIENT_PRESET',
presetId: settings.lastGradientSettingStore
});
}
//startSession patch. This function runs upon switching accounts.
Patcher.after(this.meta.name, accountSwitchModule, 'startSession', () => {
setTimeout(() => {
//If last appearance choice was a nitro client theme
if (settings.lastGradientSettingStore != -1) {
//Restore gradient on account switch
Dispatcher.dispatch({
type: 'UPDATE_BACKGROUND_GRADIENT_PRESET',
presetId: settings.lastGradientSettingStore
});
}
}, 3000);
});
} //End of clientThemes()
// #endregion
// #region Custom PFP Decode
customProfilePictureDecoding() {
Patcher.instead(this.meta.name, getAvatarUrlModule, 'getAvatarURL', (user, [userId, size, shouldAnimate], originalFunction) => {
//userpfp closer integration
//if we haven't fetched userPFP database yet and it's enabled
if ((!fetchedUserPfp || this.userPfps == undefined) && settings.userPfpIntegration) {
const userPfpJsonUrl = 'https://raw.githubusercontent.com/UserPFP/UserPFP/main/source/data.json';
// download userPfp data
Net.fetch(userPfpJsonUrl)
// parse as json
.then((res) => res.json())
// store res.avatars in this.userPfps
.then((res) => (this.userPfps = res.avatars));
//set fetchedUserPfp flag to true.
fetchedUserPfp = true;
}
//if userPfp database is not undefined, has been fetched, and is enabled
if (this.userPfps != undefined && fetchedUserPfp && settings.userPfpIntegration) {
//and this user is in the userPfp database,
if (this.userPfps[user.id] != undefined) {
//return UserPFP profile picture URL.
return this.userPfps[user.id];
}
}
//get revealed text includes P{ encoded
let revealedText = this.getRevealedText(user.id, `\uDB40\uDC50\uDB40\uDC7B`);
//if there is no 3y3 encoded text, return original function.
if (revealedText == undefined) return originalFunction(userId, size, shouldAnimate);
//This regex matches P{*} . (Do not fuck with this)
let regex = /P\{[^}]*?\}/;
//Check if there are any matches in the custom status.
let matches = revealedText.toString().match(regex);
//if not, return orig function
if (matches == undefined || matches == '') return originalFunction(userId, size, shouldAnimate);
//if there is a match, take the first match and remove the starting "P{ and ending "}"
let matchedText = matches[0].replace('P{', '').replace('}', '');
//look for a file extension. If omitted, fallback to .gif .
if (!String(matchedText).endsWith('.gif') && !String(matchedText).endsWith('.png') && !String(matchedText).endsWith('.jpg') && !String(matchedText).endsWith('.jpeg') && !String(matchedText).endsWith('.webp')) {
matchedText += '.gif'; //No supported file extension detected. Falling back to a default file extension.
}
//add this user to the list of users who have the BDNitro user badge if we haven't added them already.
if (!badgeUserIDs.includes(user.id)) badgeUserIDs.push(user.id);
//return imgur url
return `https://i.imgur.com/${matchedText}`;
});
}
// #endregion
// #region Custom PFP Encode
//Custom PFP profile customization buttons and encoding code.
async customProfilePictureEncoding(secondsightifyEncodeOnly) {
//wait for avatar customization section renderer to be loaded
await Webpack.waitForModule(Webpack.Filters.byStrings('showRemoveAvatarButton', 'onAvatarChange', 'isTryItOutFlow'));
//store avatar customization section renderer module
if (this.customPFPSettingsRenderMod == undefined)
this.customPFPSettingsRenderMod = Webpack.getMangled(/showRemoveAvatarButton:.{1,3},errors:.{1,3},onAvatarChange/, {
AvatarSection: (x) => x
});
function emptyWarn() {
UI.showToast('No URL was provided. Please enter an Imgur URL.', { type: 'warning' });
}
Patcher.after(this.meta.name, this.customPFPSettingsRenderMod, 'AvatarSection', (_, [args], ret) => {
//don't need to do anything if this is the "Try out Nitro" flow.
if (args.isTryItOutFlow) return;
ret.props.children.props.children.push(
React.createElement('input', {
id: 'profilePictureUrlInput',
style: {
width: '30%',
height: '20%',
maxHeight: '50%',
marginTop: '5px',
marginLeft: '5px'
},
placeholder: 'Imgur URL for PFP'
})
);
//Create and append Copy PFP 3y3 button.
ret.props.children.props.children.push(
React.createElement('button', {
children: 'Copy PFP 3y3',
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
id: 'profilePictureButton',
style: {
marginLeft: '10px',
whiteSpace: 'nowrap'
},
onClick: async function () {
//on copy pfp 3y3 button click
//grab text from pfp url input textarea.
let profilePictureUrlInputValue = String(document.getElementById('profilePictureUrlInput').value);
//empty, skip.
if (profilePictureUrlInputValue == undefined || profilePictureUrlInputValue == '') {
emptyWarn();
return;
}
//clean up string to encode
let stringToEncode =
'' +
profilePictureUrlInputValue
//clean up URL
.replace('http://', '') //remove protocol
.replace('https://', '')
.replace('i.imgur.com', 'imgur.com');
let encodedStr = ''; //initialize encoded string as empty string
stringToEncode = String(stringToEncode); //make doubly sure stringToEncode is a string
//if url seems correct
if (stringToEncode.toLowerCase().startsWith('imgur.com')) {
//Check for album or gallery URL
if (stringToEncode.replace('imgur.com/', '').startsWith('a/') || stringToEncode.replace('imgur.com/', '').startsWith('gallery/')) {
//Album URL, what follows is all to get the direct image link, since the album URL is not a direct link to the file.
//Fetch imgur album page
try {
const parser = new DOMParser();
stringToEncode = await Net.fetch('https://' + stringToEncode, {
method: 'GET',
mode: 'cors'
}).then((res) =>
res
.text()
//parse html, queryselect meta tag with certain name
.then((res) => parser.parseFromString(res, 'text/html').querySelector('[name="twitter:player"]').content)
);
stringToEncode = stringToEncode
.replace('http://', '') //get rid of protocol
.replace('https://', '') //get rid of protocol
.replace('i.imgur.com', 'imgur.com')
.replace('.jpg', '')
.replace('.jpeg', '')
.replace('.webp', '')
.replace('.png', '')
.replace('.mp4', '')
.replace('.webm', '')
.replace('.gifv', '')
.replace('.gif', '') //get rid of any file extension
.split('?')[0]; //remove any URL parameters since we don't want or need them
} catch (err) {
Logger.error('BDNitro', err);
UI.showToast('An error occurred. Are there multiple images in this album/gallery?', { type: 'error', forceShow: true });
return;
}
}
if (stringToEncode == '') {
UI.showToast("An error occurred: couldn't find file name.", { type: 'error', forceShow: true });
Logger.error('BDNitro', "Couldn't find file name for some reason when grabbing Imgur URL for Custom PFP. Contact srgobi!");
}
//add starting "P{" , remove "imgur.com/" , and add ending "}"
stringToEncode = 'P{' + stringToEncode.replace('imgur.com/', '') + '}';
//finally encode the string, adding a space before it so nothing fucks up
encodedStr = ' ' + secondsightifyEncodeOnly(stringToEncode);
//If this is not an Imgur URL, yell at the user.
} else if (stringToEncode.toLowerCase().startsWith('imgur.com') == false) {
UI.showToast('Please use Imgur!', { type: 'warning' });
return;
}
//if somehow none of the previous code ran, this is the last protection against an error. If this runs, something has probably gone horribly wrong.
if (encodedStr == '') return;
//copy to clipboard
try {
DiscordNative.clipboard.copy(encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
} //end copy pfp 3y3 click event
}) //end of react createElement
); //end of element push
}); //end of patch
} //End of customProfilePictureEncoding()
// #endregion
// #region Loading Badges
// Aplicar badges customizados
LoadingBadges() {
// Añadir estilos para los badges personalizados
const badgeStyles = `
div[aria-label="¡Un compañero usuario de BDNitro!"] > a > img {
content: url("https://raw.githubusercontent.com/SrGobi/BDNitro/main/badges/bd_user.svg") !important;
}
`;
DOM.addStyle('BDNitroBadges', badgeStyles);
// Configura las prioridades de las insignias
const badgeConfig = {
certified_moderator: {
id: 'certified_moderator',
icon: 'fee1624003e2fee35cb398e125dc479b',
description: 'Exalumnos de la academia de moderadores',
link: 'https://discord.com/safety',
priority: 1
},
hypesquad: {
id: 'hypesquad',
icon: 'bf01d1073931f921909045f3a39fd264',
description: 'HypeSquad Events',
link: 'https://support.discord.com/hc/en-us/articles/360035962891-Profile-Badges-101#h_01GM67K5EJ16ZHYZQ5MPRW3JT3',
priority: 2
},
hypesquad_house_1: {
id: 'hypesquad_house_1',
icon: '8a88d63823d8a71cd5e390baa45efa02',
description: 'Bravery de HypeSquad',
link: 'https://discord.com/settings/hypesquad-online',
priority: 3
},
hypesquad_house_2: {
id: 'hypesquad_house_2',
icon: '011940fd013da3f7fb926e4a1cd2e618',
description: 'House of Brilliance',
link: 'https://discord.com/settings/hypesquad-online',
priority: 4
},
hypesquad_house_3: {
id: 'hypesquad_house_3',
icon: '3aa41de486fa12454c3761e8e223442e',
description: 'Balance de HypeSquad',
link: 'https://discord.com/settings/hypesquad-online',
priority: 5
},
bug_hunter_level_1: {
id: 'bug_hunter_level_1',
icon: '2717692c7dca7289b35297368a940dd0',
description: 'Discord Bug Hunter',
link: 'https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs',
priority: 6
},
verified_developer: {
id: 'verified_developer',
icon: '6df5892e0f35b051f8b61eace34f4967',
description: 'Desarrollador inicial de bots verificado',
link: '',
priority: 7
},
NITRO: {
id: 'NITRO',
icon: '2ba85e8026a8614b640c2837bcdfe21b',
description: 'Suscriptor desde 2025',
link: 'https://github.com/srgobi/BDNitro#contributors',
priority: 8
},
bd_user: {
id: 'bd_user',
icon: '2ba85e8026a8614b640c2837bcdfe21b',
description: '¡Un compañero usuario de BDNitro!',
link: 'https://github.com/srgobi/BDNitro',
priority: 9
},
early_supporter: {
description: 'Partidario inicial',
icon: '7060786766c9c840eb3019e725d2b358',
id: 'early_supporter',
link: 'https://discord.com/settings/premium',
priority: 10
},
bronze: {
id: 'bronze',
icon: '4f33c4a9c64ce221936bd256c356f91f',
description: 'Con suscripción desde 01/01/2025',
link: 'https://discord.com/settings/premium',
priority: 11
},
silver: {
id: 'silver',
icon: '4514fab914bdbfb4ad2fa23df76121a6',
description: 'Con suscripción desde 01/01/2024',
link: 'https://discord.com/settings/premium',
priority: 12
},
gold: {
id: 'gold',
icon: '2895086c18d5531d499862e41d1155a6',
description: 'Con suscripción desde 01/01/2023',
link: 'https://discord.com/settings/premium',
priority: 13
},
platinum: {
id: 'platinum',
icon: '0334688279c8359120922938dcb1d6f8',
description: 'Con suscripción desde 01/01/2022',
link: 'https://discord.com/settings/premium',
priority: 14
},
diamond: {
id: 'diamond',
icon: '0d61871f72bb9a33a7ae568c1fb4f20a',
description: 'Con suscripción desde 01/01/2021',
link: 'https://discord.com/settings/premium',
priority: 15
},
emerald: {
id: 'emerald',
icon: '11e2d339068b55d3a506cff34d3780f3',
description: 'Con suscripción desde 01/01/2020',
link: 'https://discord.com/settings/premium',
priority: 16
},
ruby: {
id: 'ruby',
icon: 'cd5e2cfd9d7f27a8cdcd3e8a8d5dc9f4',
description: 'Con suscripción desde 01/01/2019',
link: 'https://discord.com/settings/premium',
priority: 17
},
opal: {
id: 'opal',
icon: '5b154df19c53dce2af92c9b61e6be5e2',
description: 'Con suscripción desde 01/01/2018',
link: 'https://discord.com/settings/premium',
priority: 18
}
};
// Función para decodificar badges del estado personalizado
function decodeBadges(customStatus) {
if (!customStatus || !customStatus.text) return [];
return customStatus.text.split(','); // Los badges están separados por comas
}
// Parches de insignia de perfil de usuario
Patcher.after(this.meta.name, UserProfileStore, 'getUserProfile', (_, args, ret) => {
// Comprobaciones de datos
if (!ret || !ret.userId || !ret.badges) return;
const badgesList = ret.badges.map((badge) => badge.id); // Lista de IDs de badges ya presentes
// Leer los badges codificados del estado personalizado del usuario
const customStatus = ret.customStatus || {};
const userBadges = decodeBadges(customStatus);
// Añadir los badges decodificados al perfil del usuario
userBadges.forEach((badgeId) => {
if (!badgesList.includes(badgeId) && badgeConfig[badgeId]) {
ret.badges.push(badgeConfig[badgeId]);
}
});
// Añadir insignias personalizadas a la lista si no están ya presentes
if (badgeUserIDs.includes(ret.userId) && !badgesList.includes('bd_user')) {
ret.badges.push(badgeConfig['bd_user']);
}
if (badgeUserIDs.includes(ret.userId) && settings.certified_moderator && !badgesList.includes('certified_moderator')) {
ret.badges.push(badgeConfig['certified_moderator']);
}
if (badgeUserIDs.includes(ret.userId) && settings.hypesquad && !badgesList.includes('hypesquad')) {
ret.badges.push(badgeConfig['hypesquad']);
}
if (badgeUserIDs.includes(ret.userId) && settings.hypesquad_house_1 && !badgesList.includes('hypesquad_house_1')) {
ret.badges.push(badgeConfig['hypesquad_house_1']);
}
if (badgeUserIDs.includes(ret.userId) && settings.hypesquad_house_2 && !badgesList.includes('hypesquad_house_2')) {
ret.badges.push(badgeConfig['hypesquad_house_2']);
}
if (badgeUserIDs.includes(ret.userId) && settings.hypesquad_house_3 && !badgesList.includes('hypesquad_house_3')) {
ret.badges.push(badgeConfig['hypesquad_house_3']);
}
if (badgeUserIDs.includes(ret.userId) && settings.bug_hunter_level_1 && !badgesList.includes('bug_hunter_level_1')) {
ret.badges.push(badgeConfig['bug_hunter_level_1']);
}
if (badgeUserIDs.includes(ret.userId) && settings.verified_developer && !badgesList.includes('verified_developer')) {
ret.badges.push(badgeConfig['verified_developer']);
}
if (badgeUserIDs.includes(ret.userId) && settings.NITRO && !badgesList.includes('NITRO')) {
ret.badges.push(badgeConfig['NITRO']);
}
if (badgeUserIDs.includes(ret.userId) && settings.early_supporter && !badgesList.includes('early_supporter')) {
ret.badges.push(badgeConfig['early_supporter']);
}
// Añadir el badge de Nitro seleccionado
const selectedNitroBadge = settings.nitroBadge;
const nitroBadges = ['bronze', 'silver', 'gold', 'platinum', 'diamond', 'emerald', 'ruby', 'opal'];
// Verifica si ya hay un badge de Nitro en la lista
const existingNitroBadge = badgesList.find((badgeId) => nitroBadges.includes(badgeId));
// Si no hay un badge de Nitro y hay uno seleccionado, agrégalo
if (!existingNitroBadge && selectedNitroBadge && badgeConfig[selectedNitroBadge]) {
ret.badges.push(badgeConfig[selectedNitroBadge]);
}
// Ordenar las insignias en función de su prioridad
ret.badges.sort((a, b) => {
const priorityA = badgeConfig[a.id]?.priority || Number.MAX_VALUE;
const priorityB = badgeConfig[b.id]?.priority || Number.MAX_VALUE;
return priorityA - priorityB;
});
});
} // Fin de LoadingBadges()
// #endregion
// #region 3y3 Secondsightify
secondsightifyRevealOnly(t) {
if ([...t].some((x) => 0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f)) {
// 3y3 text detected. Revealing...
return ((t) => [...t].map((x) => (0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f ? String.fromCodePoint(x.codePointAt(0) - 0xe0000) : x)).join(''))(t);
} else {
// no encoded text found, returning
return;
}
}
secondsightifyEncodeOnly(t) {
if ([...t].some((x) => 0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f)) {
// 3y3 text detected. returning...
return;
} else {
// no 3y3 text detected. encoding...
return ((t) => [...t].map((x) => (0x00 < x.codePointAt(0) && x.codePointAt(0) < 0x7f ? String.fromCodePoint(x.codePointAt(0) + 0xe0000) : x)).join(''))(t);
}
}
// #endregion
// #region 3y3 Secondsightify
secondsightifyRevealOnly(t) {
if ([...t].some((x) => 0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f)) {
// 3y3 text detected. Revealing...
return ((t) => [...t].map((x) => (0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f ? String.fromCodePoint(x.codePointAt(0) - 0xe0000) : x)).join(''))(t);
} else {
// no encoded text found, returning
return;
}
}
secondsightifyEncodeOnly(t) {
if ([...t].some((x) => 0xe0000 < x.codePointAt(0) && x.codePointAt(0) < 0xe007f)) {
// 3y3 text detected. returning...
return;
} else {
// no 3y3 text detected. encoding...
return ((t) => [...t].map((x) => (0x00 < x.codePointAt(0) && x.codePointAt(0) < 0x7f ? String.fromCodePoint(x.codePointAt(0) + 0xe0000) : x)).join(''))(t);
}
}
// #endregion
// #region Profile Effects
//Everything related to Fake Profile Effects.
async profileFX(secondsightifyEncodeOnly) {
if (settings.killProfileEffects) return; //profileFX is mutually exclusive with killProfileEffects (obviously)
//wait for profile effects module
await Webpack.waitForModule(Webpack.Filters.byKeys('profileEffects', 'tryItOutId'));
if (this.profileEffects == undefined) this.profileEffects = Webpack.getStore('ProfileEffectStore').profileEffects;
//if profile effects data hasn't been fetched by the client yet
if (this.profileEffects == undefined || this.profileEffects?.length === 0) {
//make the client fetch profile effects
await fetchProfileEffects();
this.profileEffects = Webpack.getStore('ProfileEffectStore').profileEffects;
}
let profileEffectIdList = new Array();
for (let i = 0; i < this.profileEffects.length; i++) {
profileEffectIdList.push(this.profileEffects[i].id);
}
Patcher.after(this.meta.name, UserProfileStore, 'getUserProfile', (_, [args], ret) => {
//error prevention
if (ret == undefined) return;
if (ret.bio == undefined) return;
//if bio includes encoded /fx
if (ret.bio.includes(`\uDB40\uDC2F\uDB40\uDC66\uDB40\uDC78`)) {
//reveal 3y3 encoded text. this string will also include the rest of the bio
let revealedText = this.secondsightifyRevealOnly(ret.bio);
if (revealedText == undefined) return;
//if profile effect 3y3 is detected
if (revealedText.includes('/fx')) {
const regex = /\/fx\d+/;
let matches = revealedText.toString().match(regex);
if (matches == undefined) return;
let firstMatch = matches[0];
if (firstMatch == undefined) return;
//slice the /fx and only take the number after it.
let effectIndex = parseInt(firstMatch.slice(3));
//ignore invalid data
if (isNaN(effectIndex)) return;
//ignore if the profile effect id does not point to an actual profile effect
if (profileEffectIdList[effectIndex] == undefined) return;
//set the profile effect. stringify it.
ret.profileEffectId = profileEffectIdList[effectIndex] + '';
//if for some reason we dont know what this user's ID is, stop here
if (args == undefined) return;
//otherwise add them to the list of users who show up with the BDNitro user badge
if (!badgeUserIDs.includes(args)) badgeUserIDs.push(args);
}
}
}); //end of getUserProfile patch.
//wait for profile effect section renderer to be loaded.
await Webpack.waitForModule(Webpack.Filters.byStrings('initialSelectedEffectId', 'isTryItOutFlow'));
//fetch the module now that it's loaded
if (this.profileEffectSectionRenderer == undefined)
this.profileEffectSectionRenderer = Webpack.getMangled(/isTryItOutFlow:.{1,3}=!1,initialSelectedEffectId/, {
ProfileEffectSection: (x) => x
});
//patch profile effect section renderer function to run the following code after the function runs
Patcher.after(this.meta.name, this.profileEffectSectionRenderer, 'ProfileEffectSection', (_, [args], ret) => {
const profileEffects = this.profileEffects;
function ProfileEffects({ query }) {
//if this is the tryItOut flow, don't do anything.
if (args.isTryItOutFlow) return;
let profileEffectChildren = [];
let actualRuns = 0;
//for each profile effect
for (let i = 0; i < profileEffects.length; i++) {
//get preview image url
let previewURL = profileEffects[i].config.thumbnailPreviewSrc;
let title = profileEffects[i].config.title;
//search
if (query.trim() != '') {
if (title) {
if (!title.toLowerCase().includes(query)) continue;
} else continue;
}
//encode 3y3
let encodedStr = secondsightifyEncodeOnly('/fx' + i); //fx0, fx1, etc.
//javascript that runs onclick for each profile effect button
let copyDecoration3y3 = function () {
try {
DiscordNative.clipboard.copy(' ' + encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
};
profileEffectChildren.push(
React.createElement('img', {
className: 'srgobisSecretStuff',
onClick: copyDecoration3y3,
src: previewURL,
title,
style: {
width: '22.5%',
cursor: 'pointer',
marginBottom: '0.5em',
marginLeft: '0.5em',
backgroundColor: 'var(--background-tertiary)'
}
})
);
//add newline every 4th profile effect
if ((actualRuns + 1) % 4 == 0) {
profileEffectChildren.push(React.createElement('br'));
}
actualRuns++;
}
return React.createElement('div', {
children: profileEffectChildren,
style: {
paddingTop: '10px'
}
});
}
//Profile Effects Modal
function EffectsModal() {
const [query, setQuery] = React.useState('');
return React.createElement('div', {
style: {
width: '100%',
display: 'block',
color: 'white',
whiteSpace: 'nowrap',
overflow: 'visible',
marginTop: '.5em'
},
children: [
React.createElement(Components.TextInput, {
value: query,
placeholder: 'Search...',
onChange: (input) => setQuery(input)
}),
React.createElement('br'),
React.createElement(ProfileEffects, { query })
]
});
}
//Append Change Effect button
ret.props.children.props.children.push(
//self explanatory create react element
React.createElement('button', {
children: 'Change Effect [BDNitro]',
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
size: 'bd-button-small',
id: 'changeProfileEffectButton',
style: {
width: '100px',
height: '32px',
color: 'white',
marginLeft: '10px'
},
onClick: () => {
UI.showConfirmationModal('Change Profile Effect (BDNitro)', React.createElement(EffectsModal), { cancelText: '' });
}
})
);
}); //end patch of profile effect section renderer function
} //End of profileFX()
killProfileFX() {
//self explanatory, just tries to make it so any profile that has a profile effect appears without it
Patcher.after(this.meta.name, UserProfileStore, 'getUserProfile', (_, args, ret) => {
if (ret?.profileEffectID === undefined) return;
ret.profileEffectID = undefined;
});
}
// #endregion
//fetch collectibles - decorations and nameplates are stored in data
storeProductsFromCategories = (event) => {
if (event.categories) {
event.categories.forEach((category) => {
category.products.forEach((product) => {
product.items.forEach((item) => {
if (item.asset) {
//store nameplates
if (item.asset.startsWith('nameplates')) {
data.nameplatesV2[item.skuId] = {
asset: item.asset.replace('nameplates/', ''),
palette: item.palette,
name: product.name
};
return;
} else if (item.asset.startsWith('a_')) {
//store avatar decorations assets
data.avatarDecorations[item.id] = item.asset;
return;
}
}
});
});
});
}
};
// #region Avatar Decorations
//Everything related to fake avatar decorations.
async fakeAvatarDecorations() {
//apply decorations
Patcher.after(this.meta.name, UserStore, 'getUser', (_, args, ret) => {
//basic error checking
if (args == undefined) return;
if (args[0] == undefined) return;
if (ret == undefined) return;
let avatarDecorations = data.avatarDecorations;
if (!avatarDecorations) return;
//user has an avatar decoration
if (ret.avatarDecorationData) {
//error check
if (avatarDecorations) {
//dont process fake avatar decorations
if (ret.avatarDecorationData.sku_id != '0') {
//cache avatar decoration
avatarDecorations[ret.avatarDecorationData.skuId] = ret.avatarDecorationData.asset;
}
}
}
// includes /a encoded?
let revealedText = this.getRevealedText(args[0], `\uDB40\uDC2F\uDB40\uDC61`);
//if nothing's returned, or an empty string is returned, stop processing.
if (revealedText == undefined) return;
if (revealedText == '') return;
//Matches the characters "/a" and any numbers after the a
const regex = /\/a\d+/;
let matches = revealedText.toString().match(regex);
if (matches == undefined) return;
let firstMatch = matches[0];
if (firstMatch == undefined) return;
//slice off the /a and just store the ID number
let assetId = firstMatch.slice(2);
//if this decoration is not in the list, return
if (avatarDecorations[assetId] == undefined) return;
//if this user does not have an avatar decoration, or the avatar decoration data does not match the one in the avatar decorations array,
if (ret.avatarDecorationData == undefined || ret.avatarDecorationData?.asset != avatarDecorations[assetId]) {
//set avatar decoration data to fake avatar decoration
ret.avatarDecorationData = {
asset: avatarDecorations[assetId],
sku_id: '0' //dummy sku id
};
//add user to the list of users to show with the BDNitro user badge if we haven't already.
if (!badgeUserIDs.includes(ret.id)) badgeUserIDs.push(ret.id);
}
}); //end of getUser patch for avatar decorations
//Wait for avatar decor customization section render module to be loaded.
await Webpack.waitForModule(Webpack.Filters.byStrings('userAvatarDecoration', 'guildAvatarDecoration', 'pendingAvatarDecoration'));
//Avatar decoration customization section render module/function.
if (!this.decorationCustomizationSectionMod)
this.decorationCustomizationSectionMod = Webpack.getMangled(/guildAvatarDecoration:.{1,3}?,pendingAvatarDecoration/, {
AvatarDecorationSection: (x) => x
});
//Avatar decoration customization section patch
Patcher.after(this.meta.name, this.decorationCustomizationSectionMod, 'AvatarDecorationSection', (_, [args], ret) => {
//don't run if this is the try out nitro flow.
if (args.isTryItOutFlow) return;
//push change decoration button
ret.props.children[0].props.children.push(
React.createElement('button', {
id: 'decorationButton',
children: 'Change Decoration [BDNitro]',
style: {
width: '100px',
height: '50px',
color: 'white',
borderRadius: '3px',
marginLeft: '5px'
},
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
onClick: () => {
UI.showConfirmationModal('Change Avatar Decoration (BDNitro)', React.createElement(DecorModal), { cancelText: '' });
}
})
);
const secondsightifyEncodeOnly = this.secondsightifyEncodeOnly;
function AvatarDecorations() {
if (!data.avatarDecorations) throw new Error(`Cannot possibly continue! Avatar decoration data is undefined! Did the data JSON fail to load?`);
let listOfDecorationIds = Object.keys(data.avatarDecorations);
let avatarDecorationChildren = [];
//for each avatar decoration
for (let i = 0; i < listOfDecorationIds.length; i++) {
const decorationId = listOfDecorationIds[i];
const assetHash = data.avatarDecorations[decorationId];
//remove existing nameplates from decoration list
if (assetHash.startsWith('nameplates/nameplates/')) {
delete data.avatarDecorations[decorationId];
continue;
}
//encode to 3y3 and store clipboard copy in onclick event
let encodedStr = secondsightifyEncodeOnly('/a' + decorationId); // /a[id]
//javascript that runs onclick for each avatar decoration button
let child = React.createElement('img', {
style: {
width: '23%',
cursor: 'pointer',
marginLeft: '5px',
marginBottom: '10px',
borderRadius: '4px',
backgroundColor: 'var(--background-tertiary)'
},
onClick: () => {
try {
DiscordNative.clipboard.copy(' ' + encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
},
onMouseOver: (e) => {
e.target.src = e.target.src.replace('.webp', '.png');
},
onMouseLeave: (e) => {
e.target.src = e.target.src.replace('.png', '.webp');
},
src: 'https://cdn.discordapp.com/avatar-decoration-presets/' + assetHash + '.webp?size=128'
});
avatarDecorationChildren.push(child);
//add newline every 4th decoration
if ((i + 1) % 4 == 0) {
//avatarDecorationsHTML += "
"
avatarDecorationChildren.push(React.createElement('br'));
}
}
return React.createElement('div', {
children: avatarDecorationChildren
});
}
function DecorModal() {
return React.createElement('div', {
style: {
width: '100%',
display: 'block',
color: 'white',
whiteSpace: 'nowrap',
overflow: 'visible',
marginTop: '.5em'
},
children: React.createElement(AvatarDecorations)
});
}
}); //end patch of profile decoration section renderer function
} //End of fakeAvatarDecorations()
// #endregion
//#region Emote Uploader
async UploadEmote(url, channelIdLmao, msg, emoji, runs, send) {
if (!msg[2].attachmentsToUpload) msg[2].attachmentsToUpload = [];
if (emoji === undefined) {
let emoji = { animated: true, name: 'default' };
}
if (msg === undefined) {
let msg = [channelIdLmao, { content: '' }, []];
}
let extension = '.gif';
if (!emoji.animated) {
extension = '.png';
if (!settings.PNGemote) {
extension = '.webp';
}
}
//Download emote by URL, convert to blob, then convert to File object
let file = await fetch(url)
.then((r) => r.blob())
.then((blobFile) => new File([blobFile], emoji.name + extension));
file.platform = 1; // Not exactly sure what this does, but it should be set to 1.
file.spoiler = false; //not marked as spoiler.
//Start file upload
let fileUp = new CloudUploader({ file: file, isClip: false, isThumbnail: false, platform: 1 }, channelIdLmao, false, 0);
fileUp.isImage = true;
//if this is not the first emoji uploaded
if (runs >= 1) {
//make the message attached to the upload have no text
msg[1].content = '';
//clear nonce so this is sent as a new message
msg[2].nonce = '';
//clear list of attachments
msg[2].attachmentsToUpload = [];
}
try {
//add attachment
msg[2].attachmentsToUpload.unshift(fileUp);
//send and wait till its sent before moving on
await send.apply(undefined, msg);
} catch (err) {
Logger.error(this.meta.name, err);
}
}
// #endregion
//#region Soundmoji Uploader
async UploadSoundmojis(ids, channelId, msg, sounds, send) {
if (ids != undefined && channelId != undefined && msg != undefined) {
let files = [];
for (let i = 0; i < ids.length; i++) {
let file = await fetch('https://cdn.discordapp.com/soundboard-sounds/' + ids[i])
.then((res) => res.blob())
.then((blobFile) => new File([blobFile], `${sounds[i].name}.mp3`));
file.platform = 1;
file.spoiler = false;
let fileUp = new CloudUploader({ file: file, isClip: false, isThumbnail: false, platform: 1 }, channelId, false, 0);
files.push(fileUp);
fileUp.isAudio = true;
}
if (files.length <= 10) {
try {
send(channelId, msg, { attachmentsToUpload: files }); //finally finish the process of uploading
} catch (err) {
Logger.error(this.meta.name, err);
}
} else {
//Upload 10 files at a time with a delay
let firstTime = true;
while (files.length) {
let tenFiles = files.splice(0, 10);
// uploadOptions.uploads = tenFiles;
if (!firstTime) msg.content = '';
try {
send(channelId, msg, { attachmentsToUpload: tenFiles });
} catch (err) {
Logger.error(this.meta.name, err);
}
firstTime = false;
await new Promise((r) => setTimeout(r, 3000));
}
}
}
}
// #endregion
//#region Customize Go Live V1
customizeStreamButtons() {
//Apply custom resolution and fps options for Go Live Modal V1
//This also effects Go Live Modal V2 but only after a refresh, not much I can do about that
//If you're trying to figure this shit out yourself, I recommend uncommenting the line below.
//console.log(StreamButtons);
const settings = Data.load('BDNitro', 'settings'); //just in case we can't access this;
//If custom resolution tick is disabled or custom resolution is set to 0, set it to 1440
let resolutionToSet = parseInt(settings.CustomResolution);
if (!settings.ResolutionEnabled || settings.CustomResolution == 0) resolutionToSet = 1440;
//Some of these properties are marked as read only, but they still allow you to delete them
//So any time you see "delete", what we're doing is bypassing the read-only lock by deleting it and remaking it.
//Set resolution buttons and requirements
delete ApplicationStreamResolutions.RESOLUTION_1440;
//Change 1440p resolution internally to custom resolution
ApplicationStreamResolutions.RESOLUTION_1440 = resolutionToSet;
//************************************Buttons below this point*****************************************
//Set resolution button value to custom resolution
ApplicationStreamResolutionButtons[2].value = resolutionToSet;
delete ApplicationStreamResolutionButtons[2].label;
//Set label of resolution button to custom resolution. This one is used in the popup window that appears before you start streaming.
ApplicationStreamResolutionButtons[2].label = resolutionToSet.toString();
//Set value of button with suffix label to custom resolution
ApplicationStreamResolutionButtonsWithSuffixLabel[3].value = resolutionToSet;
delete ApplicationStreamResolutionButtonsWithSuffixLabel[3].label;
//Set label of button with suffix label to custom resolution with "p" after it, ex: "1440p"
//This one is used in the dropdown kind of menu after you've started streaming
ApplicationStreamResolutionButtonsWithSuffixLabel[3].label = resolutionToSet + 'p';
let fpsToSet = parseInt(settings.CustomFPS);
//If custom FPS toggle is disabled, set to the default 60.
if (!settings.CustomFPSEnabled) fpsToSet = 60;
//set suffix label button value to the custom number
ApplicationStreamFPSButtonsWithSuffixLabel[2].value = fpsToSet;
delete ApplicationStreamFPSButtonsWithSuffixLabel[2].label;
//set button suffix label with the correct number with " FPS" after it. ex: "75 FPS". This one is used in the dropdown kind of menu
ApplicationStreamFPSButtonsWithSuffixLabel[2].label = fpsToSet + ' FPS';
//set fps button value to the correct number.
ApplicationStreamFPSButtons[2].value = fpsToSet;
delete ApplicationStreamFPSButtons[2].label;
//set fps button label to the correct number. This one is used in the popup window that appears before you start streaming.
ApplicationStreamFPSButtons[2].label = fpsToSet.toString();
ApplicationStreamFPS.FPS_60 = fpsToSet;
Data.save('BDNitro', 'settings', settings);
} //End of customizeStreamButtons()
//#endregion
// #region Emoji Bypass-related
//Whether we should skip the emoji bypass for a given emoji.
// true = skip bypass
// false = perform bypass
emojiBypassForValidEmoji(emoji, currentChannelId) {
if (settings.emojiBypassForValidEmoji) {
if (
(SelectedGuildStore.getLastSelectedGuildId() == emoji.guildId && !emoji.animated && (ChannelStore.getChannel(currentChannelId.toString()).type <= 0 || ChannelStore.getChannel(currentChannelId.toString()).type == 11) && emoji.available) ||
//If emoji is from current guild, not animated, and we are actually in a guild channel,
//and emoji is "available" (could be unavailable due to Server Boost level dropping), cancel emoji bypass
emoji.managed
) {
// OR if emoji is "managed" (emoji.managed = whether the emoji is managed by a Twitch integration)
return true;
}
}
return false;
}
_sendMessageInsteadPatch() {
this.experiments();
this.overrideExperiment('2024-11_soundmoji_sending', 2);
//#region _sendMessage Patch
Patcher.instead(this.meta.name, MessageActions, '_sendMessage', async (_, msg, send) => {
if (msg[2].poll != undefined || msg[2].activityAction != undefined || msg[2].messageReference) {
//fix polls, activity actions, forwarding
send.apply(_, msg);
return;
}
const currentChannelId = msg[0];
let emojis = [];
let emojiUrls = [];
//#region Upload Emojis
if (settings.emojiBypass && settings.emojiBypassType == 0) {
//SimpleDiscordCrypt compat
let SDCEnabled = false;
if (document.getElementsByClassName('sdc-tooltip').length > 0) {
let SDC_Tooltip = document.getElementsByClassName('sdc-tooltip')[0];
if (SDC_Tooltip.innerHTML == 'Disable Encryption') {
//SDC Encryption Enabled
SDCEnabled = true;
}
}
if (!SDCEnabled) {
msg[1].validNonShortcutEmojis.forEach(async (emoji) => {
if (this.emojiBypassForValidEmoji(emoji, currentChannelId)) return; //Unlocked emoji. Skip.
if (emoji.type == 'UNICODE') return; //If this "emoji" is actually a unicode character, it doesn't count. Skip bypassing if so.
if (emoji.guildId === undefined || emoji.id === undefined || emoji.useSpriteSheet) return; //Skip system emoji.
if (settings.PNGemote) {
emoji.forcePNG = true; //replace WEBP with PNG if the option is enabled.
}
let emojiUrl = AvatarDefaults.getEmojiURL(emoji);
if (emoji.animated) {
emojiUrl = emojiUrl.substr(0, emojiUrl.lastIndexOf('.')) + '.gif';
}
//If there is a backslash (\) before the emote we are processing,
if (msg[1].content.includes('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>')) {
//remove the backslash
msg[1].content = msg[1].content.replace('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>', '<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>');
//and skip bypass for that emote
return;
}
//remove existing URL parameters and add custom URL parameters for user's size preference. quality is always lossless.
emojiUrl = emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless&`;
//remove emote from message.
msg[1].content = msg[1].content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, '');
//queue for upload
emojis.push(emoji);
emojiUrls.push(emojiUrl);
});
}
}
//#endregion
//#region Upload Soundmojis
const channelId = msg[0];
let regex = //g;
let ids = [];
let sounds = [];
if (settings.soundmojiEnabled) {
let soundmojis = msg[1].content.match(regex);
if (soundmojis) {
for (let i = 0; i < soundmojis.length; i++) {
let id = soundmojis[i].slice(-20, -1);
let sound = SoundboardStore.getSoundById(id);
if (sound) {
sounds.push(sound);
ids.push(id);
if (sound?.emojiId == null && sound?.emojiName != null) {
//default / system emoji
msg[1].content = msg[1].content.replace(soundmojis[i], `( ${sound.emojiName} ${sound.name} )`);
} else if (sound?.emojiId != null) {
// custom emoji
let emoji = EmojiStore.getCustomEmojiById(sound.emojiId);
msg[1].content = msg[1].content.replace(soundmojis[i], `( [${emoji?.name ? emoji.name : 'someCustomEmoji'}](https://cdn.discordapp.com/emojis/${sound.emojiId}.${emoji?.animated ? 'gif' : 'png'}) ${sound.name} ) `);
} else {
//no emoji
msg[1].content = msg[1].content.replace(soundmojis[i], `( ${sound.name} ) `);
}
} else continue;
}
}
}
if (settings.emojiBypass && settings.emojiBypassType == 0) {
if (emojis.length > 0) {
//upload all emotes
for (let i = 0; i < emojis.length; i++) {
await this.UploadEmote(emojiUrls[i], currentChannelId, msg, emojis[i], i, send);
}
//reset message content since we dont want a repeated message if soundmoji upload happens next
msg[1].content = '';
}
}
if (settings.soundmojiEnabled) {
if (sounds.length > 0) await this.UploadSoundmojis(ids, channelId, msg[1], sounds, send);
}
if (settings.stickerBypass) {
let stickerIds = msg[2]?.stickerIds;
let currentChannelId = SelectedChannelStore.getChannelId();
if (stickerIds) {
for (let i = 0; i < stickerIds.length; i++) {
let stickerId = stickerIds[i];
let stickerURL = 'https://media.discordapp.net/stickers/' + stickerId + '.png?size=4096&quality=lossless';
let msgtemp = [...msg];
msgtemp[2].stickerIds = [];
if (i > 0) msgtemp[1].content = '';
if (settings.uploadStickers) {
let emoji = new Object();
emoji.animated = false;
emoji.name = 'sticker';
this.UploadEmote(stickerURL, currentChannelId, msgtemp, emoji, 0, send);
return;
} else {
let messageContent = { content: stickerURL, tts: false, invalidEmojis: [], validNonShortcutEmojis: [] };
MessageActions.sendMessage(currentChannelId, messageContent, undefined, {});
return;
}
}
}
}
if (emojis.length == 0 && sounds.length == 0) {
send.apply(_, msg);
}
});
}
emojiBypass() {
Patcher.instead(this.meta.name, isEmojiAvailableMod, 'isEmojiFilteredOrLocked', () => {
return false;
});
Patcher.instead(this.meta.name, isEmojiAvailableMod, 'isEmojiDisabled', () => {
return false;
});
Patcher.instead(this.meta.name, isEmojiAvailableMod, 'isEmojiFiltered', () => {
return false;
});
Patcher.instead(this.meta.name, isEmojiAvailableMod, 'isEmojiPremiumLocked', () => {
return false;
});
Patcher.instead(this.meta.name, isEmojiAvailableMod, 'getEmojiUnavailableReason', () => {
return;
});
//#region Ghost Mode Patch
//Ghost mode method
const ghostmodetext =
'|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| _ _ _ _ _ ';
if (settings.emojiBypassType == 1) {
function ghostModeMethod(msg, currentChannelId, self) {
if (document.getElementsByClassName('sdc-tooltip').length > 0) {
let SDC_Tooltip = document.getElementsByClassName('sdc-tooltip')[0];
if (SDC_Tooltip.innerHTML == 'Disable Encryption') {
//SDC Encryption Enabled
return;
}
}
let emojiGhostIteration = 0; // dummy value we add to the end of the URL parameters to make the same emoji appear more than once despite having the same URL.
msg.validNonShortcutEmojis.forEach((emoji) => {
if (self.emojiBypassForValidEmoji(emoji, currentChannelId)) return;
if (emoji.type == 'UNICODE') return;
if (settings.PNGemote) emoji.forcePNG = true;
let emojiUrl = AvatarDefaults.getEmojiURL(emoji);
if (emoji.guildId === undefined || emoji.id === undefined || emoji.useSpriteSheet) return; //Skip system emoji.
if (emoji.animated) {
emojiUrl = emojiUrl.substr(0, emojiUrl.lastIndexOf('.')) + '.gif';
}
if (msg.content.includes('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>')) {
msg.content = msg.content.replace('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>', '<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>');
return; //If there is a backslash before the emoji, skip it.
}
//if ghost mode is not required
if (msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, '') == '') {
msg.content = msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless& `);
return;
}
emojiGhostIteration++; //increment dummy value
//if message already has ghostmodetext.
if (msg.content.includes(ghostmodetext)) {
//remove processed emoji from the message
(msg.content = msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, '')),
//add to the end of the message
(msg.content += ' ' + emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless&${emojiGhostIteration}& `);
return;
}
//if message doesn't already have ghostmodetext, remove processed emoji and add it to the end of the message with the ghost mode text
(msg.content = msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, '')), (msg.content += ghostmodetext + '\n' + emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless& `);
});
}
//sending message in ghost mode
Patcher.before(this.meta.name, MessageActions, 'sendMessage', (_, [currentChannelId, msg]) => {
ghostModeMethod(msg, currentChannelId, this);
});
}
//#endregion
//#region Classic Mode Patch
//Original method
if (settings.emojiBypassType == 2) {
function classicModeMethod(msg, currentChannelId, self) {
if (document.getElementsByClassName('sdc-tooltip').length > 0) {
let SDC_Tooltip = document.getElementsByClassName('sdc-tooltip')[0];
if (SDC_Tooltip.innerHTML == 'Disable Encryption') {
//SDC Encryption Enabled
return;
}
}
//refer to previous bypasses for comments on what this all is for.
let emojiGhostIteration = 0;
msg.validNonShortcutEmojis.forEach((emoji) => {
if (self.emojiBypassForValidEmoji(emoji, currentChannelId)) return;
if (emoji.type == 'UNICODE') return;
if (settings.PNGemote) emoji.forcePNG = true;
let emojiUrl = AvatarDefaults.getEmojiURL(emoji);
if (emoji.guildId === undefined || emoji.id === undefined || emoji.useSpriteSheet) return; //Skip system emoji.
if (emoji.animated) {
emojiUrl = emojiUrl.substr(0, emojiUrl.lastIndexOf('.')) + '.gif';
}
if (msg.content.includes('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>')) {
msg.content = msg.content.replace('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>', '<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>');
return; //If there is a backslash before the emoji, skip it.
}
emojiGhostIteration++;
msg.content = msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless&${emojiGhostIteration}& `);
});
}
//sending message in classic mode
Patcher.before(this.meta.name, MessageActions, 'sendMessage', (_, [currentChannelId, msg]) => {
classicModeMethod(msg, currentChannelId, this);
});
//editing message in classic mode
Patcher.before(this.meta.name, MessageActions, 'editMessage', (_, obj) => {
let msg = obj[2].content;
if (msg.search(/\d{18}/g) == -1) return;
if (msg.includes(':ENC:')) return; //Fix jank with editing SimpleDiscordCrypt encrypted messages.
msg.match(/|<:.+?:\d{18}>/g).forEach((idfkAnymore) => {
obj[2].content = obj[2].content.replace(idfkAnymore, `https://cdn.discordapp.com/emojis/${idfkAnymore.match(/\d{18}/g)[0]}?size=${settings.emojiSize}&quality=lossless&`);
});
});
}
//#endregion
//#region Vencord-like Patch
//Vencord-like bypass
if (settings.emojiBypassType == 3) {
function vencordModeMethod(msg, currentChannelId, self) {
if (document.getElementsByClassName('sdc-tooltip').length > 0) {
let SDC_Tooltip = document.getElementsByClassName('sdc-tooltip')[0];
if (SDC_Tooltip.innerHTML == 'Disable Encryption') {
//SDC Encryption Enabled
return;
}
}
//refer to previous bypasses for comments on what this all is for.
let emojiGhostIteration = 0;
msg.validNonShortcutEmojis.forEach((emoji) => {
if (self.emojiBypassForValidEmoji(emoji, currentChannelId)) return;
if (emoji.type == 'UNICODE') return;
if (settings.PNGemote) emoji.forcePNG = true;
let emojiUrl = AvatarDefaults.getEmojiURL(emoji);
if (emoji.guildId === undefined || emoji.id === undefined || emoji.useSpriteSheet) return; //Skip system emoji.
if (emoji.animated) {
emojiUrl = emojiUrl.substr(0, emojiUrl.lastIndexOf('.')) + '.gif';
}
if (msg.content.includes('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>')) {
msg.content = msg.content.replace('\\<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>', '<' + emoji.allNamesString.replace(/~\b\d+\b/g, '') + emoji.id + '>');
return; //If there is a backslash before the emoji, skip it.
}
emojiGhostIteration++;
msg.content = msg.content.replace(`<${emoji.animated ? 'a' : ''}${emoji.allNamesString.replace(/~\b\d+\b/g, '')}${emoji.id}>`, `[${emoji.name}](` + emojiUrl.split('?')[0] + `?size=${settings.emojiSize}&quality=lossless&${emojiGhostIteration}&)`);
});
}
//sending message in vencord-like mode
Patcher.before(this.meta.name, MessageActions, 'sendMessage', (_, [currentChannelId, msg]) => {
vencordModeMethod(msg, currentChannelId, this);
});
}
//#endregion
} //End of emojiBypass()
//#region Fake Inline Emoji
inlineFakemojiPatch() {
//Somehow, this is the first time I've had to actually patch message rendering. (and it shows!)
Patcher.before(this.meta.name, messageRender.renderMessage, 'type', (_, [args]) => {
for (let i = 0; i < args.content.length; i++) {
let contentItem = args.content[i];
if (contentItem.type.type?.toString().includes('MASKED_LINK')) {
//is it a hyperlink?
if (contentItem.props.href.startsWith('https://cdn.discordapp.com/emojis/')) {
//does this hyperlink have an emoji URL?
let emojiName = contentItem.props?.children[0]?.props?.children;
if (emojiName == undefined) continue;
let key = contentItem.key; //store key
//create discord emoji react element
let emoteElement = React.createElement(MessageEmojiReact, {
node: {
name: `:${emojiName}:`,
src: contentItem.props.href.split('?')[0] + '?size=48',
type: 'emoji',
emojiId: contentItem.props.href.replace('https://cdn.discordapp.com/emojis/', '').split('.')[0],
animated: true,
jumboable: false //makes the emoji large or small. "jumboable" is a stupid ass name, Discord.
},
channelId: args.message.channel_id,
messageId: args.message.id,
enableClick: true //I'm curious in what circumstance this value becomes false. Does what it says on the tin; enables or disables the emoji click menu.
});
//restore key
emoteElement.key = key;
//replace this content item with our fake emoji
args.content[i] = emoteElement;
}
}
}
});
//who knows what unholy compatibility issues this will bring me
//this code fucking sucks i think
Patcher.instead(this.meta.name, renderEmbedsMod, 'renderEmbeds', (_, [message], originalFunction) => {
//get what the original function would have returned
let ret = originalFunction(message);
if (ret) {
if (ret.length > 0) {
for (let i = 0; i < ret.length; i++) {
if (ret[i]) {
if (ret[i].props?.children?.props?.embed?.image?.url) {
let url = ret[i].props.children.props.embed.image.url;
let isEmojiHyperlink = false;
//this embed is an emoji
if (url.startsWith('https://cdn.discordapp.com/emojis/')) {
/* Is embed from a hyperlink? It can't tell if it's from a hyperlink *this time*, unfortunately,
* so if someone has an emoji URL and a hyperlink with that same URL in the same message, it won't render correctly (or at least not how you might expect)!
* Let's just hope nobody notices that..! I didn't have this system initially cause I'm a dumbfuck and didn't think it over.
*/
if (message.content.includes(`](${url})`)) {
isEmojiHyperlink = true;
}
//if currently processed embed is an emoji and a hyperlink
if (isEmojiHyperlink) {
if (ret.length == 1) {
//if there is only 1 fakemoji
//removes first instance of pattern [anyemojiname](https://cdn.discordapp.com/emojis/anynumber.ext) then checks if there is anything else in the message
if (
message.content
.replace(/\[.*?\]\(https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(png|webp|gif).*?\)/, '') //is regex necessary? probably.
.trim().length > 0
) {
//if there is other stuff in the message, delete the embed
delete ret[i];
}
//if there is 1 fakemoji and nothing else in the message, it will keep the regular embed (default behavior)
//for some reason, if the fakemoji is in a message alone, it disappears, so keeping the embed was the easiest solution
}
//if there is more than 1 hyperlink
else {
delete ret[i]; //if the hyperlink is an emoji url, delete the embed
}
}
}
}
}
}
}
//removes empty items from the array. def did not take from stackoverflow (trust)
ret = ret.filter((n) => n);
return ret;
} else {
//if the original function returns undefined/null
//this should never happen, but in case it does, return an empty array
return [];
}
});
//#endregion
}
//#endregion
//#region Video Quality Patch
videoQualityModule() {
//Custom Bitrates, FPS, Resolution
Patcher.before(this.meta.name, videoOptionFunctions, 'updateVideoQuality', (e) => {
if (settings.CustomBitrateEnabled) {
if (settings.minBitrate > 0) {
//Minimum Bitrate
e.videoQualityManager.options.videoBitrateFloor = settings.minBitrate * 1000;
e.videoQualityManager.options.videoBitrate.min = settings.minBitrate * 1000;
e.videoQualityManager.options.desktopBitrate.min = settings.minBitrate * 1000;
} else {
e.videoQualityManager.options.videoBitrateFloor = 5e5;
e.videoQualityManager.options.videoBitrate.min = 5e5;
e.videoQualityManager.options.desktopBitrate.min = 5e5;
}
if (settings.targetBitrate > 0) {
//Target Bitrate
e.videoQualityManager.options.desktopBitrate.target = settings.targetBitrate * 1000;
}
if (settings.maxBitrate > 0) {
//Maximum Bitrate
e.videoQualityManager.options.videoBitrate.max = settings.maxBitrate * 1000;
e.videoQualityManager.options.desktopBitrate.max = settings.maxBitrate * 1000;
e.videoQualityManager.goliveMaxQuality.bitrateMax = settings.maxBitrate * 1000;
}
}
if (settings.voiceBitrate > -1) {
//Audio Bitrate
e.voiceBitrate = settings.voiceBitrate * 1000;
e.conn.setTransportOptions({
encodingVoiceBitRate: e.voiceBitrate
});
}
//Video quality bypasses if Custom FPS is enabled.
if (settings.CustomFPSEnabled) {
e.videoQualityManager.options.videoBudget.framerate = e.videoStreamParameters[0].maxFrameRate;
e.videoQualityManager.options.videoCapture.framerate = e.videoStreamParameters[0].maxFrameRate;
}
//If screen sharing bypasses are enabled,
if (settings.screenSharing) {
//Ensure video quality parameters match the stream parameters.
const videoQuality = new Object({
width: e.videoStreamParameters[0].maxResolution.width,
height: e.videoStreamParameters[0].maxResolution.height,
framerate: e.videoStreamParameters[0].maxFrameRate
});
e.remoteSinkWantsMaxFramerate = e.videoStreamParameters[0].maxFrameRate;
//janky fix to #218
if (videoQuality.height <= 0) {
videoQuality.height = 1440;
}
if (videoQuality.width <= 0) {
videoQuality.width = 2160;
if (parseInt(e.videoStreamParameters[0].maxResolution.height * (16 / 9) > 2160 * (16 / 9))) videoQuality.width = parseInt(e.videoStreamParameters[0].maxResolution.height * (16 / 9));
}
//Ensure video budget and capture quality parameters match stream parameters
e.videoQualityManager.options.videoBudget = videoQuality;
e.videoQualityManager.options.videoCapture = videoQuality;
//Ladder bypasses
let pixelBudget = videoQuality.width * videoQuality.height;
e.videoQualityManager.ladder.pixelBudget = pixelBudget;
e.videoQualityManager.ladder.ladder = LadderModule.calculateLadder(pixelBudget);
e.videoQualityManager.ladder.orderedLadder = LadderModule.calculateOrderedLadder(e.videoQualityManager.ladder.ladder);
}
});
} //End of videoQualityModule()
//#endregion
async stickerSending() {
Patcher.instead(this.meta.name, MessageActions, 'sendStickers', (_, args, originalFunction) => {
let stickerID = args[1][0];
let stickerURL = 'https://media.discordapp.net/stickers/' + stickerID + '.png?size=4096&quality=lossless';
let currentChannelId = SelectedChannelStore.getChannelId();
if (settings.uploadStickers) {
let emoji = new Object();
emoji.animated = false;
emoji.name = args[0];
let msg = [undefined, { content: '' }];
this.UploadEmote(stickerURL, currentChannelId, msg, emoji, 1, send);
return;
}
if (!settings.uploadStickers) {
let messageContent = { content: stickerURL, tts: false, invalidEmojis: [], validNonShortcutEmojis: [] };
MessageActions.sendMessage(currentChannelId, messageContent, undefined, {});
}
});
}
//#region 3y3 Profile Colors
decodeAndApplyProfileColors() {
Patcher.after(this.meta.name, UserProfileStore, 'getUserProfile', (_, args, ret) => {
if (ret == undefined) return;
if (ret.bio == null) return;
const colorString = ret.bio.match(/\u{e005b}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e002c}\u{e0023}([\u{e0061}-\u{e0066}\u{e0041}-\u{e0046}\u{e0030}-\u{e0039}]+?)\u{e005d}/u);
if (colorString == null) return;
let parsed = [...colorString[0]].map((c) => String.fromCodePoint(c.codePointAt(0) - 0xe0000)).join('');
let colors = parsed
.substring(1, parsed.length - 1)
.split(',')
.map((x) => parseInt(x.replace('#', '0x'), 16));
ret.themeColors = colors;
ret.premiumType = 2;
});
}
//Everything that has to do with the GUI and encoding of the fake profile colors 3y3 shit.
//Replaced DOM manipulation with React patching 4/2/2024
async encodeProfileColors() {
//wait for theme color picker module to be loaded
await Webpack.waitForModule(Webpack.Filters.byKeys('getTryItOutThemeColors'));
//wait for color picker renderer module to be loaded
await Webpack.waitForModule(Webpack.Filters.byStrings('__invalid_profileThemesSection'));
if (this.colorPickerRendererMod == undefined)
this.colorPickerRendererMod = Webpack.getMangled('__invalid_profileThemesSection', {
ProfileThemesSection: (x) => x
});
Patcher.after(this.meta.name, this.colorPickerRendererMod, 'ProfileThemesSection', (_, args, ret) => {
ret.props.children.props.children.push(
//append copy colors 3y3 button
React.createElement('button', {
id: 'copy3y3button',
children: 'Copy Colors 3y3',
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
style: {
marginLeft: '10px',
marginTop: '10px'
},
onClick: () => {
let themeColors;
themeColors = UserSettingsAccountStore.getAllPending().pendingThemeColors;
if (!themeColors) themeColors = UserSettingsAccountStore.getAllTryItOut().tryItOutThemeColors;
if (!themeColors) {
UI.showToast('Nothing has been copied. Is the selected color identical to your current color?', { type: 'warning' });
return;
}
const primary = themeColors[0];
const accent = themeColors[1];
let message = `[#${primary.toString(16).padStart(6, '0')},#${accent.toString(16).padStart(6, '0')}]`;
const padding = '';
let encoded = Array.from(message)
.map((x) => x.codePointAt(0))
.filter((x) => x >= 0x20 && x <= 0x7f)
.map((x) => String.fromCodePoint(x + 0xe0000))
.join('');
let encodedStr = (padding || '') + ' ' + encoded;
try {
DiscordNative.clipboard.copy(encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
}
})
);
});
} //End of encodeProfileColors()
//#endregion
//#region Banner Decoding
//Decode 3y3 from profile bio and apply fake banners.
bannerUrlDecoding() {
let endpoint, bucket, prefix, usrBgData;
//if userBg integration is enabled, and we havent already downloaded & parsed userBg data,
if (settings.userBgIntegration && !fetchedUserBg) {
//userBg database url.
const userBgJsonUrl = 'https://usrbg.is-hardly.online/users';
//download, then store json
Net.fetch(userBgJsonUrl, { timeout: 100000 }).then((res) =>
res.json().then((res) => {
usrBgData = res;
endpoint = res.endpoint;
bucket = res.bucket;
prefix = res.prefix;
//mark db as fetched so we only fetch it once per load of the plugin
fetchedUserBg = true;
})
);
}
//Patch getUserBannerURL function
Patcher.before(this.meta.name, AvatarDefaults, 'getUserBannerURL', (_, args) => {
args[0].canAnimate = true;
});
//Patch getBannerURL function
Patcher.instead(this.meta.name, getBannerURL, 'getBannerURL', (user, [args], ogFunction) => {
let profile = user._userProfile;
//Returning ogFunction with the same arguments that were passed to this function will do the vanilla check for a legit banner.
if (profile == undefined) return ogFunction(args);
if (settings.userBgIntegration) {
//if userBg integration is enabled
//if we've fetched the userbg database
if (fetchedUserBg) {
//if user is in userBg database,
if (usrBgData?.users[user.userId]) {
profile.banner = 'funky_kong_is_epic'; //set banner id to fake value
profile.premiumType = 2; //set this profile to appear with premium rendering
return `${endpoint}/${bucket}/${prefix}${user.userId}?${usrBgData?.users[user.userId]}`; //return userBg banner URL and exit.
}
}
}
//do original function if we don't have the user's bio
if (profile.bio == undefined) return ogFunction(args);
// includes /B encoded?
if (profile.bio.includes(`\uDB40\uDC42\uDB40\uDC7B`)) {
//reveal 3y3 encoded text, store as parsed
let parsed = this.secondsightifyRevealOnly(profile.bio);
//if there is no 3y3 encoded text, return original function
if (parsed == undefined) return ogFunction(args);
//This regex matches B{*} . Do not touch unless you know what you are doing.
let regex = /B\{[^}]*?\}/;
//find banner url in parsed bio
let matches = parsed.toString().match(regex);
//if there's no matches, return original function
if (matches == undefined) return ogFunction(args);
if (matches == '') return ogFunction(args);
//if there is matched text, grab the first match, replace the starting "B{" and ending "}" to get the clean filename
let matchedText = matches[0].replace('B{', '').replace('}', '');
//Checking for file extension.
if (!String(matchedText).endsWith('.gif') && !String(matchedText).endsWith('.png') && !String(matchedText).endsWith('.jpg') && !String(matchedText).endsWith('.jpeg') && !String(matchedText).endsWith('.webp')) {
matchedText += '.gif'; //Fallback to a default file extension if one is not found.
}
//set banner id to fake value
profile.banner = 'funky_kong_is_epic';
//set this profile to appear with premium rendering
profile.premiumType = 2;
//add this user to the list of users that show with the BDNitro user badge if we haven't aleady.
if (!badgeUserIDs.includes(user.userId)) badgeUserIDs.push(user.userId);
//return final banner URL.
return `https://i.imgur.com/${matchedText}`;
}
}); //End of patch for getBannerURL
} //End of bannerUrlDecoding()
//#endregion
//#region Banner Encoding
//Make buttons in profile customization settings, encode imgur URLs and copy to clipboard
//Documented/commented and partially rewritten to use React patching on 3/6/2024
async bannerUrlEncoding(secondsightifyEncodeOnly) {
//wait for banner customization renderer module to be loaded
await Webpack.waitForModule(Webpack.Filters.byStrings('showRemoveBannerButton', 'isTryItOutFlow', 'buttonsContainer'));
if (this.profileBannerSectionRenderer == undefined)
this.profileBannerSectionRenderer = Webpack.getMangled(/showRemoveBannerButton:.{1,3}?,errors:.{1,3}?,onBannerChange/, {
BannerSection: (x) => x
});
function emptyWarn() {
UI.showToast('No URL was provided. Please enter an Imgur URL.', { type: 'warning' });
}
Patcher.after(this.meta.name, this.profileBannerSectionRenderer, 'BannerSection', (_, args, ret) => {
//create and append profileBannerUrlInput input element.
let profileBannerUrlInput = React.createElement('input', {
id: 'profileBannerUrlInput',
placeholder: 'Imgur URL for Banner',
style: {
float: 'right',
width: '30%',
height: '20%',
maxHeight: '50%',
marginTop: 'auto',
marginBottom: 'auto',
marginLeft: '10px'
}
});
ret.props.children.props.children.push(profileBannerUrlInput);
ret.props.children.props.children.push(
//append Copy 3y3 button
//create react element
React.createElement('button', {
id: 'profileBannerButton',
children: 'Copy Banner 3y3',
className: `${buttonClassModule.button} ${buttonClassModule.lookFilled} ${buttonClassModule.colorBrand} ${buttonClassModule.sizeSmall} ${buttonClassModule.grow}`,
size: 'bd-button-small',
style: {
whiteSpace: 'nowrap',
marginLeft: '10px'
},
onClick: async function () {
//Upon clicking Copy 3y3 button
//grab text from banner URL input textarea
let profileBannerUrlInputValue = String(document.getElementById('profileBannerUrlInput').value);
//if it's empty, stop processing and issue a warning.
if (profileBannerUrlInputValue == undefined) {
emptyWarn();
return;
}
if (profileBannerUrlInputValue == '') {
emptyWarn();
return;
}
//clean up string to encode
let stringToEncode =
'' +
profileBannerUrlInputValue
.replace('http://', '') //get rid of protocol
.replace('https://', '')
.replace('.jpg', '')
.replace('.png', '')
.replace('.mp4', '')
.replace('webm', '')
.replace('i.imgur.com', 'imgur.com'); //change i.imgur.com to imgur.com
let encodedStr = ''; //initialize encoded string as empty string
stringToEncode = String(stringToEncode); //make doubly sure stringToEncode is a string
//if url seems correct
if (stringToEncode.toLowerCase().startsWith('imgur.com')) {
//Check for album or gallery URL
if (stringToEncode.replace('imgur.com/', '').startsWith('a/') || stringToEncode.replace('imgur.com/', '').startsWith('gallery/')) {
//Album URL, what follows is all to get the direct image link, since the album URL is not a direct link to the file.
//Fetch imgur album page
try {
const parser = new DOMParser();
stringToEncode = await Net.fetch('https://' + stringToEncode, {
method: 'GET',
mode: 'cors'
}).then((res) =>
res
.text()
//parse html, queryselect meta tag with certain name
.then((res) => parser.parseFromString(res, 'text/html').querySelector('[name="twitter:player"]').content)
);
stringToEncode = stringToEncode
.replace('http://', '') //get rid of protocol
.replace('https://', '') //get rid of protocol
.replace('i.imgur.com', 'imgur.com')
.replace('.jpg', '')
.replace('.jpeg', '')
.replace('.webp', '')
.replace('.png', '')
.replace('.mp4', '')
.replace('.webm', '')
.replace('.gifv', '')
.replace('.gif', '') //get rid of any file extension
.split('?')[0]; //remove any URL parameters since we don't want or need them
} catch (err) {
Logger.error('BDNitro', err);
UI.showToast('An error occurred. Are there multiple images in this album/gallery?', { type: 'error', forceShow: true });
return;
}
}
if (stringToEncode == '') {
UI.showToast("An error occurred: couldn't find file name.", { type: 'error', forceShow: true });
Logger.error('BDNitro', "Couldn't find file name when trying to grab Imgur URL for Profile Banner for some reason. Contact srgobi.");
return;
}
//add starting "B{" , remove "imgur.com/" , and add ending "}"
stringToEncode = 'B{' + stringToEncode.replace('imgur.com/', '') + '}';
//finally encode the string, adding a space before it so nothing fucks up
encodedStr = ' ' + secondsightifyEncodeOnly(stringToEncode);
//If this is not an Imgur URL, yell at the user.
} else if (stringToEncode.toLowerCase().startsWith('imgur.com') == false) {
UI.showToast('Please use Imgur!', { type: 'warning' });
return;
}
//if somehow none of the previous code ran, this is the last protection against an error. If this runs, something has probably gone horribly wrong.
if (encodedStr == '') return;
//copy to clipboard
try {
DiscordNative.clipboard.copy(encodedStr);
UI.showToast('3y3 copied to clipboard!', { type: 'info' });
} catch (err) {
UI.showToast('Failed to copy to clipboard!', { type: 'error', forceShow: true });
Logger.error('BDNitro', err);
}
} //end of onClick function
}) //end of react createElement
); //end of profileBannerButton element push
}); //end of patched function
} //End of bannerUrlEncoding()
//#endregion
//#region App Icons
appIcons() {
//technically don't need this anymore but i'll leave it in for the sake of redundancy
Patcher.before(this.meta.name, appIconButtonsModule, 'CTAButtons', (_, args) => {
args[0].disabled = false; //force buttons clickable
});
Patcher.instead(this.meta.name, AppIcon, 'AppIconHome', (_, __, originalFunction) => {
const currentDesktopIcon = CurrentDesktopIcon.getCurrentDesktopIcon();
if (currentDesktopIcon == 'AppIcon') {
return React.createElement(RegularAppIcon, {
size: 'custom',
color: 'currentColor',
width: 30,
height: 30
});
} else {
return React.createElement(CustomAppIcon, {
id: currentDesktopIcon,
width: 48
});
}
});
}
//#endregion
//#region Meta and Updates
parseMeta(fileContent) {
//zlibrary code
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/;
const escapedAtRegex = /^\\@/;
const block = fileContent.split('/**', 2)[1].split('*/', 1)[0];
const out = {};
let field = '';
let accum = '';
for (const line of block.split(splitRegex)) {
if (line.length === 0) continue;
if (line.charAt(0) === '@' && line.charAt(1) !== ' ') {
out[field] = accum;
const l = line.indexOf(' ');
field = line.substring(1, l);
accum = line.substring(l + 1);
} else {
accum += ' ' + line.replace('\\n', '\n').replace(escapedAtRegex, '@');
}
}
out[field] = accum.trim();
delete out[''];
out.format = 'jsdoc';
return out;
}
async checkForUpdate() {
try {
let res = await fetch(this.meta.updateUrl);
if (!res.ok && res.status != 200) {
Logger.warn('BDNitro', res);
res = await Net.fetch(this.meta.updateUrl);
if (!res.ok && res.status != 200) {
Logger.error('BDNitro', res);
throw new Error('Failed to check for updates!');
}
}
let fileContent = await res.text();
let remoteMeta = this.parseMeta(fileContent);
let remoteVersion = remoteMeta.version.trim().split('.');
let currentVersion = this.meta.version.trim().split('.');
if (parseInt(remoteVersion[0]) > parseInt(currentVersion[0])) {
this.newUpdateNotify(remoteMeta, fileContent);
} else if (remoteVersion[0] == currentVersion[0] && parseInt(remoteVersion[1]) > parseInt(currentVersion[1])) {
this.newUpdateNotify(remoteMeta, fileContent);
} else if (remoteVersion[0] == currentVersion[0] && remoteVersion[1] == currentVersion[1] && parseInt(remoteVersion[2]) > parseInt(currentVersion[2])) {
this.newUpdateNotify(remoteMeta, fileContent);
}
} catch (err) {
UI.showToast('[BDNitro] Failed to check for updates', { type: 'error' });
Logger.error(this.meta.name, err);
}
}
newUpdateNotify(remoteMeta, remoteFile) {
Logger.info(this.meta.name, 'A new update is available!');
UI.showConfirmationModal('Update Available', [`Update ${remoteMeta.version} is now available for BDNitro!`, 'Press Download Now to update!'], {
confirmText: 'Download Now',
onConfirm: async (e) => {
if (remoteFile) {
await new Promise((r) => fs.writeFile(path.join(Plugins.folder, `${this.meta.name}.plugin.js`), remoteFile, r));
try {
let currentVersionInfo = Data.load(this.meta.name, 'currentVersionInfo');
currentVersionInfo.hasShownChangelog = false;
Data.save(this.meta.name, 'currentVersionInfo', currentVersionInfo);
} catch (err) {
UI.showToast('An error occurred when trying to download the update!', { type: 'error', forceShow: true });
}
}
}
});
}
//#endregion
saveDataFile() {
const dataFilePath = path.join(Plugins.folder, `${this.meta.name}.data.json`);
try {
fs.writeFileSync(dataFilePath, JSON.stringify(data));
} catch (err) {
UI.showToast(`[${this.meta.name}] Error saving dava JSON. See console for error message.`, { type: 'error', forceShow: true });
Logger.error(this.meta.name, err);
}
}
loadDataFile() {
try {
const dataFilePath = path.join(Plugins.folder, `${this.meta.name}.data.json`);
if (!fs.existsSync(dataFilePath)) {
fs.writeFileSync(dataFilePath, '{}');
}
try {
data = Object.assign({}, defaultData, JSON.parse(fs.readFileSync(dataFilePath)));
} catch (err) {
UI.showToast(`[${this.meta.name}] Error parsing or reading data JSON.`, { type: 'error', forceShow: true });
Logger.warn(this.meta.name, 'Error parsing or reading data JSON.');
Logger.warn(this.meta.name, err);
data = {};
}
} catch (err) {
UI.showToast(`[${this.meta.name}] An error occurred loading the data file.`, { type: 'error', forceShow: true });
Logger.error(this.meta.name, 'An error occurred loading the data file.');
Logger.error(this.meta.name, err);
}
}
//#region Start, Stop
start() {
Logger.info(this.meta.name, '(v' + this.meta.version + ') has started.');
try {
//load settings from config
settings = Object.assign({}, defaultSettings, Data.load(this.meta.name, 'settings'));
} catch (err) {
//The super mega awesome data-unfucker 9000
Logger.warn(this.meta.name, err);
Logger.info(this.meta.name, 'Error parsing JSON. Resetting file to default...');
//watch this shit yo
fs.rmSync(path.join(Plugins.folder, `${this.meta.name}.config.json`));
Plugins.reload(this.meta.name);
Plugins.enable(this.meta.name);
return;
}
this.loadDataFile();
//update check
try {
let currentVersionInfo = {};
try {
currentVersionInfo = Object.assign({}, { version: this.meta.version, hasShownChangelog: false }, Data.load('BDNitro', 'currentVersionInfo'));
} catch (err) {
currentVersionInfo = { version: this.meta.version, hasShownChangelog: false };
}
if (this.meta.version != currentVersionInfo.version) currentVersionInfo.hasShownChangelog = false;
currentVersionInfo.version = this.meta.version;
Data.save(this.meta.name, 'currentVersionInfo', currentVersionInfo);
if (settings.checkForUpdates) this.checkForUpdate();
if (!currentVersionInfo.hasShownChangelog) {
UI.showChangelogModal({
title: 'BDNitro Changelog',
subtitle: config.changelog[0].title,
changes: [
{
title: config.changelog[0].title,
type: 'changed',
items: config.changelog[0].items
}
]
});
currentVersionInfo.hasShownChangelog = true;
Data.save(this.meta.name, 'currentVersionInfo', currentVersionInfo);
}
} catch (err) {
Logger.error(this.meta.name, err);
}
this.saveAndUpdate();
}
stop() {
CurrentUser.premiumType = ORIGINAL_NITRO_STATUS;
Patcher.unpatchAll(this.meta.name);
Dispatcher.unsubscribe('COLLECTIBLES_CATEGORIES_FETCH_SUCCESS', this.storeProductsFromCategories);
DOM.removeStyle(this.meta.name);
DOM.removeStyle('BDNitroBadges');
let ffmpegScript = document.getElementById('ffmpegScript');
if (ffmpegScript) {
ffmpegScript.remove();
}
Data.save('BDNitro', 'settings', settings);
this.saveDataFile();
Logger.info(this.meta.name, '(v' + this.meta.version + ') has stopped.');
}
// #endregion
};
// #endregion
/*@end@*/