Compare commits

...

2 commits

Author SHA1 Message Date
Nuckyz 426c949ee4
Future proof ContextMenuAPI against mangled export 2024-06-19 23:50:04 -03:00
Nuckyz a01ee40591
Clean-up related additions to mangled exports 2024-06-19 23:50:04 -03:00
19 changed files with 256 additions and 182 deletions

View file

@ -121,7 +121,7 @@ export function findGroupChildrenByChildId(id: string | string[], children: Arra
} }
interface ContextMenuProps { interface ContextMenuProps {
contextMenuApiArguments?: Array<any>; contextMenuAPIArguments?: Array<any>;
navId: string; navId: string;
children: Array<ReactElement | null>; children: Array<ReactElement | null>;
"aria-label": string; "aria-label": string;
@ -135,7 +135,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
children: cloneMenuChildren(props.children), children: cloneMenuChildren(props.children),
}; };
props.contextMenuApiArguments ??= []; props.contextMenuAPIArguments ??= [];
const contextMenuPatches = navPatches.get(props.navId); const contextMenuPatches = navPatches.get(props.navId);
if (!Array.isArray(props.children)) props.children = [props.children]; if (!Array.isArray(props.children)) props.children = [props.children];
@ -143,7 +143,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
if (contextMenuPatches) { if (contextMenuPatches) {
for (const patch of contextMenuPatches) { for (const patch of contextMenuPatches) {
try { try {
patch(props.children, ...props.contextMenuApiArguments); patch(props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err); ContextMenuLogger.error(`Patch for ${props.navId} errored,`, err);
} }
@ -152,7 +152,7 @@ export function _usePatchContextMenu(props: ContextMenuProps) {
for (const patch of globalPatches) { for (const patch of globalPatches) {
try { try {
patch(props.navId, props.children, ...props.contextMenuApiArguments); patch(props.navId, props.children, ...props.contextMenuAPIArguments);
} catch (err) { } catch (err) {
ContextMenuLogger.error("Global patch errored,", err); ContextMenuLogger.error("Global patch errored,", err);
} }

View file

@ -1,69 +0,0 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
import { Settings } from "./Settings";
interface Setting<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T | ((old: T) => T)): Promise<void>;
/**
* React hook for automatically updating components when the setting is updated
*/
useSetting(): T;
settingsStoreApiGroup: string;
settingsStoreApiName: string;
}
export const SettingsStores: Array<Setting<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"') as any;
if (modId == null) return new Logger("SettingsStoreAPI").error("Didn't find stores module.");
const mod = wreq(modId);
if (mod == null) return;
return Object.values(mod).filter((s: any) => s?.settingsStoreApiGroup) as any;
});
/**
* Get the store for a setting
* @param group The setting group
* @param name The name of the setting
*/
export function getSettingStore<T = any>(group: string, name: string): Setting<T> | undefined {
if (!Settings.plugins.SettingsStoreAPI.enabled) throw new Error("Cannot use SettingsStoreAPI without setting as dependency.");
return SettingsStores?.find(s => s?.settingsStoreApiGroup === group && s?.settingsStoreApiName === name);
}
/**
* getSettingStore but lazy
*/
export function getSettingStoreLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getSettingStore<T>(group, name));
}

View file

@ -0,0 +1,83 @@
/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2023 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { proxyLazy } from "@utils/lazy";
import { Logger } from "@utils/Logger";
import { findModuleId, proxyLazyWebpack, wreq } from "@webpack";
import { Settings } from "./Settings";
interface UserSettingDefinition<T> {
/**
* Get the setting value
*/
getSetting(): T;
/**
* Update the setting value
* @param value The new value
*/
updateSetting(value: T): Promise<void>;
/**
* Update the setting value
* @param value A callback that accepts the old value as the first argument, and returns the new value
*/
updateSetting(value: (old: T) => T): Promise<void>;
/**
* Stateful React hook for this setting value
*/
useSetting(): T;
userSettingDefinitionsAPIGroup: string;
userSettingDefinitionsAPIName: string;
}
export const UserSettingsDefinitions: Record<PropertyKey, UserSettingDefinition<any>> | undefined = proxyLazyWebpack(() => {
const modId = findModuleId('"textAndImages","renderSpoilers"');
if (modId == null) return new Logger("UserSettingDefinitionsAPI").error("Didn't find settings definitions module.");
return wreq(modId as any);
});
/**
* Get the definition for a setting.
*
* @param group The setting group
* @param name The name of the setting
*/
export function getUserSettingDefinition<T = any>(group: string, name: string): UserSettingDefinition<T> | undefined {
if (!Settings.plugins.UserSettingDefinitionsAPI.enabled) throw new Error("Cannot use UserSettingDefinitionsAPI without setting as dependency.");
for (const key in UserSettingsDefinitions) {
const userSettingDefinition = UserSettingsDefinitions[key];
if (userSettingDefinition.userSettingDefinitionsAPIGroup === group && userSettingDefinition.userSettingDefinitionsAPIName === name) {
return userSettingDefinition;
}
}
}
/**
* {@link getUserSettingDefinition}, lazy.
*
* Get the definition for a setting.
*
* @param group The setting group
* @param name The name of the setting
*/
export function getUserSettingDefinitionLazy<T = any>(group: string, name: string) {
return proxyLazy(() => getUserSettingDefinition<T>(group, name));
}

View file

@ -31,8 +31,8 @@ import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications"; import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList"; import * as $ServerList from "./ServerList";
import * as $Settings from "./Settings"; import * as $Settings from "./Settings";
import * as $SettingsStores from "./SettingsStores";
import * as $Styles from "./Styles"; import * as $Styles from "./Styles";
import * as $UserSettingDefinitions from "./UserSettingDefinitions";
/** /**
* An API allowing you to listen to Message Clicks or run your own logic * An API allowing you to listen to Message Clicks or run your own logic
@ -118,4 +118,7 @@ export const ChatButtons = $ChatButtons;
*/ */
export const MessageUpdater = $MessageUpdater; export const MessageUpdater = $MessageUpdater;
export const SettingsStores = $SettingsStores; /**
* An API allowing you to get the definition for an user setting
*/
export const UserSettingDefinitions = $UserSettingDefinitions;

View file

@ -18,6 +18,38 @@
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
import { filters, waitFor, waitForSubscriptions } from "@webpack";
/**
* The last var name which the ContextMenu module was WebpackRequire'd and assigned to
*/
let lastVarName = "";
/**
* The key exporting the ContextMenu module "Menu"
*/
let exportKey: PropertyKey = "";
/**
* The id of the module exporting the ContextMenu module "Menu"
*/
let modId: PropertyKey = "";
let mangledCallback: (...args: any[]) => any;
waitFor(filters.byCode("Menu API only allows Items and groups of Items as children."), mangledCallback = (_, modInfo) => {
exportKey = modInfo.exportKey;
modId = modInfo.id;
waitForSubscriptions.delete(nonMangledCallback);
});
let nonMangledCallback: (...args: any[]) => any;
waitFor(filters.byProps("Menu", "MenuItem"), nonMangledCallback = (_, modInfo) => {
exportKey = "Menu";
modId = modInfo.id;
waitForSubscriptions.delete(mangledCallback);
});
export default definePlugin({ export default definePlugin({
name: "ContextMenuAPI", name: "ContextMenuAPI",
@ -34,12 +66,26 @@ export default definePlugin({
} }
}, },
{ {
find: ".Menu,{", find: "navId:",
all: true, all: true,
replacement: { noWarn: true,
match: /Menu,{(?<=\.jsxs?\)\(\i\.Menu,{)/g, replacement: [
replace: "$&contextMenuApiArguments:typeof arguments!=='undefined'?arguments:[]," {
} get match() {
return RegExp(`${String(modId)}(?<=(\\i)=.+?)`);
},
replace: (id, varName) => {
lastVarName = varName;
return id;
}
},
{
get match() {
return RegExp(`${String(exportKey)},{(?<=${lastVarName}\\.${String(exportKey)},{)`, "g");
},
replace: "$&contextMenuAPIArguments:typeof arguments!=='undefined'?arguments:[],"
}
]
} }
] ]
}); });

View file

@ -20,23 +20,30 @@ import { Devs } from "@utils/constants";
import definePlugin from "@utils/types"; import definePlugin from "@utils/types";
export default definePlugin({ export default definePlugin({
name: "SettingsStoreAPI", name: "UserSettingDefinitionsAPI",
description: "Patches Discord's SettingsStores to expose their group and name", description: "Patches Discord's UserSettingDefinitions to expose their group and name.",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
patches: [ patches: [
{ {
find: ",updateSetting:", find: ",updateSetting:",
replacement: [ replacement: [
// Main setting definition
{ {
match: /(?<=INFREQUENT_USER_ACTION.{0,20}),useSetting:/, match: /(?<=INFREQUENT_USER_ACTION.{0,20},)useSetting:/,
replace: ",settingsStoreApiGroup:arguments[0],settingsStoreApiName:arguments[1]$&" replace: "userSettingDefinitionsAPIGroup:arguments[0],userSettingDefinitionsAPIName:arguments[1],$&"
}, },
// some wrapper. just make it copy the group and name // Selective wrapper
{ {
match: /updateSetting:.{0,20}shouldSync/, match: /updateSetting:.{0,100}SELECTIVELY_SYNCED_USER_SETTINGS_UPDATE/,
replace: "settingsStoreApiGroup:arguments[0].settingsStoreApiGroup,settingsStoreApiName:arguments[0].settingsStoreApiName,$&" replace: "userSettingDefinitionsAPIGroup:arguments[0].userSettingDefinitionsAPIGroup,userSettingDefinitionsAPIName:arguments[0].userSettingDefinitionsAPIName,$&"
},
// Override wrapper
{
match: /updateSetting:.{0,60}USER_SETTINGS_OVERRIDE_CLEAR/,
replace: "userSettingDefinitionsAPIGroup:arguments[0].userSettingDefinitionsAPIGroup,userSettingDefinitionsAPIName:arguments[0].userSettingDefinitionsAPIName,$&"
} }
] ]
} }
] ]

View file

@ -5,7 +5,7 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores"; import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions";
import { ImageIcon } from "@components/Icons"; import { ImageIcon } from "@components/Icons";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { getCurrentGuild, openImageModal } from "@utils/discord"; import { getCurrentGuild, openImageModal } from "@utils/discord";
@ -15,7 +15,7 @@ import { Clipboard, GuildStore, Menu, PermissionStore } from "@webpack/common";
const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild"); const GuildSettingsActions = findByPropsLazy("open", "selectRole", "updateGuild");
const DeveloperMode = getSettingStoreLazy("appearance", "developerMode")!; const DeveloperMode = getUserSettingDefinitionLazy("appearance", "developerMode")!;
function PencilIcon() { function PencilIcon() {
return ( return (
@ -65,7 +65,7 @@ export default definePlugin({
name: "BetterRoleContext", name: "BetterRoleContext",
description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile", description: "Adds options to copy role color / edit role / view role icon when right clicking roles in the user profile",
authors: [Devs.Ven, Devs.goodbee], authors: [Devs.Ven, Devs.goodbee],
dependencies: ["SettingsStoreAPI"], dependencies: ["UserSettingDefinitionsAPI"],
settings, settings,

View file

@ -17,7 +17,7 @@
*/ */
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores"; import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions";
import { ErrorCard } from "@components/ErrorCard"; import { ErrorCard } from "@components/ErrorCard";
import { Link } from "@components/Link"; import { Link } from "@components/Link";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -33,8 +33,7 @@ const useProfileThemeStyle = findByCodeLazy("profileThemeStyle:", "--profile-gra
const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile"); const ActivityComponent = findComponentByCodeLazy("onOpenGameProfile");
const ActivityClassName = findByPropsLazy("activity", "buttonColor"); const ActivityClassName = findByPropsLazy("activity", "buttonColor");
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingDefinitionLazy<boolean>("status", "showCurrentGame")!;
async function getApplicationAsset(key: string): Promise<string> { async function getApplicationAsset(key: string): Promise<string> {
if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, ""); if (/https?:\/\/(cdn|media)\.discordapp\.(com|net)\/attachments\//.test(key)) return "mp:" + key.replace(/https?:\/\/(cdn|media)\.discordapp\.(com|net)\//, "");
@ -394,7 +393,7 @@ export default definePlugin({
name: "CustomRPC", name: "CustomRPC",
description: "Allows you to set a custom rich presence.", description: "Allows you to set a custom rich presence.",
authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev], authors: [Devs.captain, Devs.AutumnVN, Devs.nin0dev],
dependencies: ["SettingsStoreAPI"], dependencies: ["UserSettingDefinitionsAPI"],
start: setRpc, start: setRpc,
stop: () => setRpc(true), stop: () => setRpc(true),
settings, settings,

View file

@ -17,8 +17,8 @@
*/ */
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores";
import { disableStyle, enableStyle } from "@api/Styles"; import { disableStyle, enableStyle } from "@api/Styles";
import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import definePlugin, { OptionType } from "@utils/types"; import definePlugin, { OptionType } from "@utils/types";
@ -28,7 +28,7 @@ import style from "./style.css?managed";
const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:"); const Button = findComponentByCodeLazy("Button.Sizes.NONE,disabled:");
const ShowCurrentGame = getSettingStoreLazy<boolean>("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingDefinitionLazy<boolean>("status", "showCurrentGame")!;
function makeIcon(showCurrentGame?: boolean) { function makeIcon(showCurrentGame?: boolean) {
const { oldIcon } = settings.use(["oldIcon"]); const { oldIcon } = settings.use(["oldIcon"]);
@ -87,7 +87,7 @@ export default definePlugin({
name: "GameActivityToggle", name: "GameActivityToggle",
description: "Adds a button next to the mic and deafen button to toggle game activity.", description: "Adds a button next to the mic and deafen button to toggle game activity.",
authors: [Devs.Nuckyz, Devs.RuukuLada], authors: [Devs.Nuckyz, Devs.RuukuLada],
dependencies: ["SettingsStoreAPI"], dependencies: ["UserSettingDefinitionsAPI"],
settings, settings,
patches: [ patches: [

View file

@ -6,7 +6,7 @@
import * as DataStore from "@api/DataStore"; import * as DataStore from "@api/DataStore";
import { definePluginSettings, Settings } from "@api/Settings"; import { definePluginSettings, Settings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores"; import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex"; import { Flex } from "@components/Flex";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
@ -28,7 +28,7 @@ interface IgnoredActivity {
const RunningGameStore = findStoreLazy("RunningGameStore"); const RunningGameStore = findStoreLazy("RunningGameStore");
const ShowCurrentGame = getSettingStoreLazy("status", "showCurrentGame")!; const ShowCurrentGame = getUserSettingDefinitionLazy("status", "showCurrentGame")!;
function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) { function ToggleIcon(activity: IgnoredActivity, tooltipText: string, path: string, fill: string) {
return ( return (
@ -208,7 +208,7 @@ export default definePlugin({
name: "IgnoreActivities", name: "IgnoreActivities",
authors: [Devs.Nuckyz], authors: [Devs.Nuckyz],
description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.", description: "Ignore activities from showing up on your status ONLY. You can configure which ones are specifically ignored from the Registered Games and Activities tabs, or use the general settings below.",
dependencies: ["SettingsStoreAPI"], dependencies: ["UserSettingDefinitionsAPI"],
settings, settings,

View file

@ -19,7 +19,7 @@
import { addAccessory, removeAccessory } from "@api/MessageAccessories"; import { addAccessory, removeAccessory } from "@api/MessageAccessories";
import { updateMessage } from "@api/MessageUpdater"; import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { getSettingStoreLazy } from "@api/SettingsStores"; import { getUserSettingDefinitionLazy } from "@api/UserSettingDefinitions";
import ErrorBoundary from "@components/ErrorBoundary"; import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js"; import { Devs } from "@utils/constants.js";
import { classes } from "@utils/misc"; import { classes } from "@utils/misc";
@ -54,7 +54,7 @@ const ChannelMessage = findComponentByCodeLazy("childrenExecutedCommand:", ".hid
const SearchResultClasses = findByPropsLazy("message", "searchResult"); const SearchResultClasses = findByPropsLazy("message", "searchResult");
const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor"); const EmbedClasses = findByPropsLazy("embedAuthorIcon", "embedAuthor", "embedAuthor");
const MessageDisplayCompact = getSettingStoreLazy("textAndImages", "messageDisplayCompact")!; const MessageDisplayCompact = getUserSettingDefinitionLazy("textAndImages", "messageDisplayCompact")!;
const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g; const messageLinkRegex = /(?<!<)https?:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/(?:\d{17,20}|@me)\/(\d{17,20})\/(\d{17,20})/g;
const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//; const tenorRegex = /^https:\/\/(?:www\.)?tenor\.com\//;
@ -366,7 +366,7 @@ export default definePlugin({
name: "MessageLinkEmbeds", name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message", description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev], authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI", "SettingsStoreAPI"], dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI", "UserSettingDefinitionsAPI"],
settings, settings,

View file

@ -120,7 +120,7 @@ export function openImageModal(url: string, props?: Partial<React.ComponentProps
placeholder={url} placeholder={url}
src={url} src={url}
renderLinkComponent={props => <MaskedLink {...props} />} renderLinkComponent={props => <MaskedLink {...props} />}
// FIXME: wtf is this? do we need to pass some proper component?? // Don't render forward message button
renderForwardComponent={() => null} renderForwardComponent={() => null}
shouldHideMediaOptions={false} shouldHideMediaOptions={false}
shouldAnimate shouldAnimate

View file

@ -20,9 +20,9 @@ export * from "./classes";
export * from "./components"; export * from "./components";
export * from "./menu"; export * from "./menu";
export * from "./react"; export * from "./react";
export * from "./settingsStores";
export * from "./stores"; export * from "./stores";
export * as ComponentTypes from "./types/components.d"; export * as ComponentTypes from "./types/components.d";
export * as MenuTypes from "./types/menu.d"; export * as MenuTypes from "./types/menu.d";
export * as UtilTypes from "./types/utils.d"; export * as UtilTypes from "./types/utils.d";
export * from "./userSettings";
export * from "./utils"; export * from "./utils";

View file

@ -21,6 +21,5 @@ export * from "./components";
export * from "./fluxEvents"; export * from "./fluxEvents";
export * from "./i18nMessages"; export * from "./i18nMessages";
export * from "./menu"; export * from "./menu";
export * from "./settingsStores";
export * from "./stores"; export * from "./stores";
export * from "./utils"; export * from "./utils";

View file

@ -1,11 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export interface SettingsStore<T = any> {
getSetting(): T;
updateSetting(value: T): void;
useSetting(): T;
}

View file

@ -82,7 +82,7 @@ interface RestRequestData {
retries?: number; retries?: number;
} }
export type RestAPI = Record<"delete" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>; export type RestAPI = Record<"del" | "get" | "patch" | "post" | "put", (data: RestRequestData) => Promise<any>>;
export type Permissions = "CREATE_INSTANT_INVITE" export type Permissions = "CREATE_INSTANT_INVITE"
| "KICK_MEMBERS" | "KICK_MEMBERS"

View file

@ -24,7 +24,7 @@ import { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { patches } from "../plugins"; import { patches } from "../plugins";
import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from "."; import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, waitForSubscriptions, wreq } from ".";
const logger = new Logger("WebpackInterceptor", "#8caaee"); const logger = new Logger("WebpackInterceptor", "#8caaee");
const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/);
@ -204,8 +204,7 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
} }
exports = module.exports; exports = module.exports;
if (exports == null) return;
if (!exports) return;
// There are (at the time of writing) 11 modules exporting the window // There are (at the time of writing) 11 modules exporting the window
// Make these non enumerable to improve webpack search performance // Make these non enumerable to improve webpack search performance
@ -240,32 +239,37 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
for (const callback of moduleListeners) { for (const callback of moduleListeners) {
try { try {
callback(exports, id); callback(exports, { id, factory: originalMod });
} catch (err) { } catch (err) {
logger.error("Error in Webpack module listener:\n", err, callback); logger.error("Error in Webpack module listener:\n", err, callback);
} }
} }
for (const [filter, callback] of subscriptions) { for (const [filter, callback] of waitForSubscriptions) {
try { try {
if (exports && filter(exports)) { if (filter(exports)) {
subscriptions.delete(filter); waitForSubscriptions.delete(filter);
callback(exports, id); callback(exports, { id, factory: originalMod, exportKey: "" });
} else if (typeof exports === "object") { continue;
if (exports.default && filter(exports.default)) { }
subscriptions.delete(filter);
callback(exports.default, id); if (typeof exports !== "object") continue;
} else {
for (const nested in exports) if (nested.length <= 3) { if (exports.default != null && filter(exports.default)) {
if (exports[nested] && filter(exports[nested])) { waitForSubscriptions.delete(filter);
subscriptions.delete(filter); callback(exports.default, { id, factory: originalMod, exportKey: "default" });
callback(exports[nested], id); continue;
} }
}
for (const key in exports) if (key.length <= 3) {
if (exports[key] != null && filter(exports[key])) {
waitForSubscriptions.delete(filter);
callback(exports[key], { id, factory: originalMod, exportKey: key });
break;
} }
} }
} catch (err) { } catch (err) {
logger.error("Error while firing callback for Webpack subscription:\n", err, filter, callback); logger.error("Error while firing callback for Webpack waitFor subscription:\n", err, filter, callback);
} }
} }
} as any as { toString: () => string, original: any, (...args: any[]): void; }; } as any as { toString: () => string, original: any, (...args: any[]): void; };

View file

@ -68,11 +68,21 @@ export const filters = {
} }
}; };
export type CallbackFn = (mod: any, id: string) => void; export type ModListenerInfo = {
id: PropertyKey;
factory: (module: any, exports: any, require: WebpackInstance) => void;
};
export type ModCallbackInfo = ModListenerInfo & {
exportKey: PropertyKey;
};
export type ModListenerFn = (module: any, info: ModListenerInfo) => void;
export type ModCallbackFn = (module: any, info: ModCallbackInfo) => void;
export const subscriptions = new Map<FilterFn, CallbackFn>();
export const moduleListeners = new Set<CallbackFn>();
export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>(); export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>();
export const moduleListeners = new Set<ModListenerFn>();
export const waitForSubscriptions = new Map<FilterFn, ModCallbackFn>();
export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>(); export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>();
export function _initWebpack(webpackRequire: WebpackInstance) { export function _initWebpack(webpackRequire: WebpackInstance) {
@ -106,7 +116,7 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (!mod.loaded || !mod?.exports) continue; if (!mod?.loaded || mod?.exports == null) continue;
if (filter(mod.exports)) { if (filter(mod.exports)) {
return isWaitFor ? [mod.exports, key] : mod.exports; return isWaitFor ? [mod.exports, key] : mod.exports;
@ -114,16 +124,13 @@ export const find = traceFunction("find", function find(filter: FilterFn, { isIn
if (typeof mod.exports !== "object") continue; if (typeof mod.exports !== "object") continue;
if (mod.exports.default && filter(mod.exports.default)) { if (mod.exports.default != null && filter(mod.exports.default)) {
const found = mod.exports.default; return isWaitFor ? [mod.exports.default, key] : mod.exports.default;
return isWaitFor ? [found, key] : found;
} }
// the length check makes search about 20% faster for (const key in mod.exports) if (key.length <= 3) {
for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { if (mod.exports[key] != null && filter(mod.exports[key])) {
const nested = mod.exports[nestedMod]; return isWaitFor ? [mod.exports[key], key] : mod.exports[key];
if (nested && filter(nested)) {
return isWaitFor ? [nested, key] : nested;
} }
} }
} }
@ -142,18 +149,25 @@ export function findAll(filter: FilterFn) {
const ret = [] as any[]; const ret = [] as any[];
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (!mod.loaded || !mod?.exports) continue; if (!mod?.loaded || mod?.exports == null) continue;
if (filter(mod.exports)) if (filter(mod.exports)) {
ret.push(mod.exports); ret.push(mod.exports);
else if (typeof mod.exports !== "object")
continue; continue;
}
if (mod.exports.default && filter(mod.exports.default)) if (typeof mod.exports !== "object") continue;
if (mod.exports.default != null && filter(mod.exports.default)) {
ret.push(mod.exports.default); ret.push(mod.exports.default);
else for (const nestedMod in mod.exports) if (nestedMod.length <= 3) { continue;
const nested = mod.exports[nestedMod]; }
if (nested && filter(nested)) ret.push(nested);
for (const key in mod.exports) if (key.length <= 3) {
if (mod.exports[key] && filter(mod.exports[key])) {
ret.push(mod.exports[key]);
break;
}
} }
} }
@ -190,7 +204,7 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
outer: outer:
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (!mod.loaded || !mod?.exports) continue; if (!mod.loaded || mod?.exports == null) continue;
for (let j = 0; j < length; j++) { for (let j = 0; j < length; j++) {
const filter = filters[j]; const filter = filters[j];
@ -204,26 +218,23 @@ export const findBulk = traceFunction("findBulk", function findBulk(...filterFns
break; break;
} }
if (typeof mod.exports !== "object") if (typeof mod.exports !== "object") continue;
continue;
if (mod.exports.default && filter(mod.exports.default)) { if (mod.exports.default && filter(mod.exports.default)) {
results[j] = mod.exports.default; results[j] = mod.exports.default;
filters[j] = undefined; filters[j] = undefined;
if (++found === length) break outer; if (++found === length) break outer;
break; continue;
} }
for (const nestedMod in mod.exports) for (const key in mod.exports) if (key.length <= 3) {
if (nestedMod.length <= 3) { if (mod.exports[key] && filter(mod.exports[key])) {
const nested = mod.exports[nestedMod]; results[j] = mod.exports[key];
if (nested && filter(nested)) { filters[j] = undefined;
results[j] = nested; if (++found === length) break outer;
filters[j] = undefined; break;
if (++found === length) break outer;
continue outer;
}
} }
}
} }
} }
@ -293,7 +304,7 @@ export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "f
* Note that the example below exists already as an api, see {@link findByPropsLazy} * Note that the example below exists already as an api, see {@link findByPropsLazy}
* @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah);
*/ */
export function proxyLazyWebpack<T = any>(factory: () => any, attempts?: number) { export function proxyLazyWebpack<T = any>(factory: () => T, attempts?: number) {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy<T>(factory, attempts); return proxyLazy<T>(factory, attempts);
@ -446,25 +457,27 @@ export function findExportedComponentLazy<T extends object = any>(...props: stri
* }) * })
*/ */
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> { export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string, mappers: Record<S, FilterFn>): Record<S, any> {
const exports = {} as Record<S, any>; const mapping = {} as Record<S, any>;
const id = findModuleId(code); const id = findModuleId(code);
if (id === null) if (id === null) return mapping;
return exports;
const exports = wreq(id as any);
const mod = wreq(id as any);
outer: outer:
for (const key in mod) { for (const key in exports) {
const member = mod[key]; const value = exports[key];
for (const newName in mappers) { for (const newName in mappers) {
// if the current mapper matches this module // if the current mapper matches this module
if (mappers[newName](member)) { if (mappers[newName](value)) {
exports[newName] = member; mapping[newName] = value;
continue outer; continue outer;
} }
} }
} }
return exports;
return mapping;
}); });
/** /**
@ -570,7 +583,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac
* Wait for a module that matches the provided filter to be registered, * Wait for a module that matches the provided filter to be registered,
* then call the callback with the module as the first argument * then call the callback with the module as the first argument
*/ */
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) { export function waitFor(filter: string | string[] | FilterFn, callback: ModCallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]); if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string") if (typeof filter === "string")
@ -585,7 +598,7 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback
if (existing) return void callback(existing, id); if (existing) return void callback(existing, id);
} }
subscriptions.set(filter, callback); waitForSubscriptions.set(filter, callback);
} }
/** /**