diff --git a/renderer/src/modules/api/webpack.js b/renderer/src/modules/api/webpack.js index 7ac28e20..e0f0df6a 100644 --- a/renderer/src/modules/api/webpack.js +++ b/renderer/src/modules/api/webpack.js @@ -1,6 +1,18 @@ import Logger from "common/logger"; import WebpackModules, {Filters} from "../webpackmodules"; +const getOptions = (args, defaultOptions = {}) => { + if (args.length > 1 && + typeof(args[args.length - 1]) === "object" && + !Array.isArray(args[args.length - 1]) && + args[args.length - 1] !== null + ) { + Object.assign(defaultOptions, args.pop()); + } + + return defaultOptions; +}; + /** * `Webpack` is a utility class for getting internal webpack modules. Instance is accessible through the {@link BdApi}. * This is extremely useful for interacting with the internals of Discord. @@ -9,6 +21,10 @@ import WebpackModules, {Filters} from "../webpackmodules"; * @name Webpack */ const Webpack = { + /** + * A Proxy that returns the module source by ID. + */ + modules: WebpackModules.modules, /** * Series of {@link Filters} to be used for finding webpack modules. @@ -16,19 +32,29 @@ const Webpack = { * @memberof Webpack */ Filters: { + /** + * @deprecated + */ + byProps(...props) {return Filters.byKeys(props);}, + /** * Generates a function that filters by a set of properties. - * @param {...string} props List of property names + * @param {...string} keys List of property names * @returns {function} A filter that checks for a set of properties */ - byProps(...props) {return Filters.byProps(props);}, + byKeys(...keys) {return Filters.byKeys(keys);}, + + /** + * @deprecated + */ + byPrototypeFields(...props) {return Filters.byPrototypeKeys(props);}, /** * Generates a function that filters by a set of properties on the object's prototype. * @param {...string} props List of property names * @returns {function} A filter that checks for a set of properties on the object's prototype. */ - byPrototypeFields(...props) {return Filters.byPrototypeFields(props);}, + byPrototypeKeys(...props) {return Filters.byPrototypeKeys(props);}, /** * Generates a function that filters by a regex. @@ -52,6 +78,13 @@ const Webpack = { */ byDisplayName(name) {return Filters.byDisplayName(name);}, + /** + * Generates a function that filters by a specific internal Store name. + * @param {string} name Name the store should have + * @returns {function} A filter that checks for a Store name match + */ + byStoreName(name) {return Filters.byStoreName(name);}, + /** * Generates a combined function from a list of filters. * @param {...function} filters A list of filters @@ -60,6 +93,22 @@ const Webpack = { combine(...filters) {return Filters.combine(...filters);}, }, + /** + * Searches for a module by value, returns module & matched key. Useful in combination with the Patcher. + * @param {(value: any, index: number, array: any[]) => boolean} filter A function to use to filter the module + * @param {object} [options] Set of options to customize the search + * @param {any} [options.target=null] Optional module target to look inside. + * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export + * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack export getters. + * @return {[Any, string]} + */ + getWithKey(filter, options = {}) { + if (("first" in options)) return Logger.error("BdApi.Webpack~getWithKey", "Unsupported option first."); + if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~getWithKey", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); + if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getWithKey", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); + return WebpackModules.getWithKey(filter, options); + }, + /** * Finds a module using a filter function. * @memberof Webpack @@ -71,12 +120,26 @@ const Webpack = { * @return {any} */ getModule(filter, options = {}) { - if (("first" in options) && typeof(options.first) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.first", options.first, "boolean expected."); + if (("first" in options) && typeof(options.first) !== "boolean") return Logger.error("BdApi.Webpack~get", "Unsupported type used for options.first", options.first, "boolean expected."); if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); return WebpackModules.getModule(filter, options); }, + /** + * Finds all modules matching a filter function. + * @param {Function} filter A function to use to filter modules + * @param {object} [options] Options to configure the search + * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export + * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack exports + * @return {any[]} + */ + getModules(filter, options = {}) { + if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~getModules", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); + if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getModules", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); + return WebpackModules.getModule(filter, Object.assign(options, {first: false})); + }, + /** * Finds multiple modules using multiple filters. * @memberof Webpack @@ -102,9 +165,106 @@ const Webpack = { waitForModule(filter, options = {}) { if (("defaultExport" in options) && typeof(options.defaultExport) !== "boolean") return Logger.error("BdApi.Webpack~waitForModule", "Unsupported type used for options.defaultExport", options.defaultExport, "boolean expected."); if (("signal" in options) && !(options.signal instanceof AbortSignal)) return Logger.error("BdApi.Webpack~waitForModule", "Unsupported type used for options.signal", options.signal, "AbortSignal expected."); - if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~getModule", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); + if (("searchExports" in options) && typeof(options.searchExports) !== "boolean") return Logger.error("BdApi.Webpack~waitForModule", "Unsupported type used for options.searchExports", options.searchExports, "boolean expected."); return WebpackModules.getLazy(filter, options); }, + + /** + * Finds a module using its code. + * @param {RegEx} regex A regular expression to use to filter modules + * @param {object} [options] Options to configure the search + * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export + * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack exports + * @return {Any} + */ + getByRegex(regex, options = {}) { + return WebpackModules.getModule(Filters.byRegex(regex), options); + }, + + /** + * Finds all modules using its code. + * @param {RegEx} regex A regular expression to use to filter modules + * @param {object} [options] Options to configure the search + * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export + * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack exports + * @return {Any[]} + */ + getAllByRegex(regex, options = {}) { + return WebpackModules.getModule(Filters.byRegex(regex), Object.assign({}, options, {first: true})); + }, + + /** + * Finds a single module using properties on its prototype. + * @param {...string} prototypes Properties to use to filter modules + * @return {Any} + */ + getByPrototypeKeys(...prototypes) { + const options = getOptions(prototypes); + + return WebpackModules.getModule(Filters.byPrototypeKeys(prototypes), options); + }, + + /** + * Finds all modules with a set of properties of its prototype. + * @param {...string} prototypes Properties to use to filter modules + * @return {Any[]} + */ + getAllByPrototypeKeys(...prototypes) { + const options = getOptions(prototypes, {first: false}); + + return WebpackModules.getModule(Filters.byPrototypeKeys(prototypes), options); + }, + + /** + * Finds a single module using its own properties. + * @param {...string} props Properties to use to filter modules + * @return {Any} + */ + getByKeys(...props) { + const options = getOptions(props); + + return WebpackModules.getModule(Filters.byKeys(props), options); + }, + + /** + * Finds all modules with a set of properties. + * @param {...string} props Properties to use to filter modules + * @return {Any[]} + */ + getAllByKeys(...props) { + const options = getOptions(props, {first: false}); + + return WebpackModules.getModule(Filters.byKeys(props), options); + }, + + /** + * Finds a single module using a set of strings. + * @param {...String} props Strings to use to filter modules + * @return {Any} + */ + getByStrings(...strings) { + const options = getOptions(strings); + + return WebpackModules.getModule(Filters.byStrings(...strings), options); + }, + + /** + * Finds all modules with a set of strings. + * @param {...String} strings Strings to use to filter modules + * @return {Any[]} + */ + getAllByStrings(...strings) { + const options = getOptions(strings, {first: false}); + + return WebpackModules.getModule(Filters.byStrings(...strings), options); + }, + + /** + * Finds an internal Store module using the name. + * @param {String} name Name of the store to find (usually includes "Store") + * @return {Any} + */ + getStore(name) {return WebpackModules.getModule(Filters.byStoreName(name));}, }; Object.freeze(Webpack); diff --git a/renderer/src/modules/webpackmodules.js b/renderer/src/modules/webpackmodules.js index 051ed72e..5bcd0bc7 100644 --- a/renderer/src/modules/webpackmodules.js +++ b/renderer/src/modules/webpackmodules.js @@ -22,7 +22,7 @@ export class Filters { * @param {module:WebpackModules.Filters~filter} filter - Additional filter * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties */ - static byProps(props, filter = m => m) { + static byKeys(props, filter = m => m) { return module => { if (!module) return false; if (typeof(module) !== "object" && typeof(module) !== "function") return false; @@ -41,7 +41,7 @@ export class Filters { * @param {module:WebpackModules.Filters~filter} filter - Additional filter * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties on the object's prototype */ - static byPrototypeFields(fields, filter = m => m) { + static byPrototypeKeys(fields, filter = m => m) { return module => { if (!module) return false; if (typeof(module) !== "object" && typeof(module) !== "function") return false; @@ -94,7 +94,6 @@ export class Filters { /** * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties. * @param {string} name - Name the module should have - * @param {module:WebpackModules.Filters~filter} filter - Additional filter * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties */ static byDisplayName(name) { @@ -103,6 +102,17 @@ export class Filters { }; } + /** + * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties. + * @param {string} name - Name the store should have (usually includes the word Store) + * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties + */ + static byStoreName(name) { + return module => { + return module?._dispatchToken && module?.getName?.() === name; + }; + } + /** * Generates a combined {@link module:WebpackModules.Filters~filter} from a list of filters. * @param {...module:WebpackModules.Filters~filter} filters - A list of filters @@ -140,6 +150,25 @@ export default class WebpackModules { static findByUniqueProperties(props, first = true) {return first ? this.getByProps(...props) : this.getAllByProps(...props);} static findByDisplayName(name) {return this.getByDisplayName(name);} + /** + * A Proxy that returns the module source by ID. + */ + static modules = new Proxy({}, { + ownKeys() {return Object.keys(WebpackModules.require.m);}, + getOwnPropertyDescriptor() { + return { + enumerable: true, + configurable: true, // Not actually + }; + }, + get(_, k) { + return WebpackModules.require.m[k]; + }, + set() { + throw new Error("[WebpackModules~modules] Setting modules is not allowed."); + } + }); + /** * Finds a module using a filter function. * @param {function} filter A function to use to filter modules @@ -252,6 +281,24 @@ export default class WebpackModules { return returnedModules; } + /** + * Searches for a module by value, returns module & matched key. Useful in combination with the Patcher. + * @param {(value: any, index: number, array: any[]) => boolean} filter A function to use to filter the module + * @param {object} [options] Set of options to customize the search + * @param {any} [options.target=null] Optional module target to look inside. + * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export + * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack export getters. + * @return {[Any, string]} + */ + static *getWithKey(filter, {target = null, ...rest} = {}) { + yield target ??= this.getModule(exports => + Object.values(exports).some(filter), + rest + ); + + yield target && Object.keys(target).find(k => filter(target[k])); + } + /** * Finds all modules matching a filter function. * @param {Function} filter A function to use to filter modules @@ -283,7 +330,7 @@ export default class WebpackModules { * @return {Any} */ static getByPrototypes(...prototypes) { - return this.getModule(Filters.byPrototypeFields(prototypes)); + return this.getModule(Filters.byPrototypeKeys(prototypes)); } /** @@ -292,7 +339,7 @@ export default class WebpackModules { * @return {Any} */ static getAllByPrototypes(...prototypes) { - return this.getModule(Filters.byPrototypeFields(prototypes), {first: false}); + return this.getModule(Filters.byPrototypeKeys(prototypes), {first: false}); } /** @@ -301,7 +348,7 @@ export default class WebpackModules { * @return {Any} */ static getByProps(...props) { - return this.getModule(Filters.byProps(props)); + return this.getModule(Filters.byKeys(props)); } /** @@ -310,7 +357,7 @@ export default class WebpackModules { * @return {Any} */ static getAllByProps(...props) { - return this.getModule(Filters.byProps(props), {first: false}); + return this.getModule(Filters.byKeys(props), {first: false}); } /** diff --git a/renderer/src/ui/settings.js b/renderer/src/ui/settings.js index 3d6c9f4d..977f31dc 100644 --- a/renderer/src/ui/settings.js +++ b/renderer/src/ui/settings.js @@ -62,7 +62,7 @@ export default new class SettingsRenderer { } async patchSections() { - const UserSettings = await WebpackModules.getLazy(Filters.byPrototypeFields(["getPredicateSections"])); + const UserSettings = await WebpackModules.getLazy(Filters.byPrototypeKeys(["getPredicateSections"])); Patcher.after("SettingsManager", UserSettings.prototype, "getPredicateSections", (thisObject, args, returnValue) => { let location = returnValue.findIndex(s => s.section.toLowerCase() == "changelog") - 1;