fix issues & crashes caused by recent Discord changes (#3231)

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
v 2025-02-17 02:10:02 +01:00 committed by GitHub
parent 8dfbb5f9d8
commit 6b0c02daa6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 82 additions and 46 deletions

View file

@ -78,9 +78,9 @@ async function runReporter() {
result = await Webpack.extractAndLoadChunks(code, matcher); result = await Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null; if (result === false) result = null;
} else if (method === "mapMangledModule") { } else if (method === "mapMangledModule") {
const [code, mapper] = args; const [code, mapper, includeBlacklistedExports] = args;
result = Webpack.mapMangledModule(code, mapper); result = Webpack.mapMangledModule(code, mapper, includeBlacklistedExports);
if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail"); if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail");
} else { } else {
// @ts-ignore // @ts-ignore

View file

@ -158,6 +158,9 @@ export default definePlugin({
aboveActivity: getIntlMessage("ACTIVITY_SETTINGS") aboveActivity: getIntlMessage("ACTIVITY_SETTINGS")
}; };
if (!names[settingsLocation] || names[settingsLocation].endsWith("_SETTINGS"))
return firstChild === "PREMIUM";
return header === names[settingsLocation]; return header === names[settingsLocation];
} catch { } catch {
return firstChild === "PREMIUM"; return firstChild === "PREMIUM";

View file

@ -58,9 +58,9 @@ export const { match, P }: Pick<typeof import("ts-pattern"), "match" | "P"> = ma
export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep"); export const lodash: typeof import("lodash") = findByPropsLazy("debounce", "cloneDeep");
export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', { export const i18n = mapMangledModuleLazy('defaultLocale:"en-US"', {
t: filters.byProps(runtimeHashMessageKey("DISCORD")),
intl: filters.byProps("string", "format"), intl: filters.byProps("string", "format"),
t: filters.byProps(runtimeHashMessageKey("DISCORD")) }, true);
});
export let SnowflakeUtils: t.SnowflakeUtils; export let SnowflakeUtils: t.SnowflakeUtils;
waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m); waitFor(["fromTimestamp", "extractTimestamp"], m => SnowflakeUtils = m);

View file

@ -12,7 +12,7 @@ import { canonicalizeReplacement } from "@utils/patches";
import { Patch, PatchReplacement } from "@utils/types"; import { Patch, PatchReplacement } from "@utils/types";
import { traceFunctionWithResults } from "../debug/Tracer"; import { traceFunctionWithResults } from "../debug/Tracer";
import { _initWebpack, _shouldIgnoreModule, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack"; import { _blacklistBadModules, _initWebpack, factoryListeners, findModuleId, moduleListeners, waitForSubscriptions, wreq } from "./webpack";
import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleExports, PatchedModuleFactory, WebpackRequire } from "./wreq.d"; import { AnyModuleFactory, AnyWebpackRequire, MaybePatchedModuleFactory, ModuleExports, PatchedModuleFactory, WebpackRequire } from "./wreq.d";
export const patches = [] as Patch[]; export const patches = [] as Patch[];
@ -190,6 +190,20 @@ define(Function.prototype, "m", {
*/ */
define(this, "m", { value: proxiedModuleFactories }); define(this, "m", { value: proxiedModuleFactories });
// Overwrite Webpack's defineExports function to define the export descriptors configurable.
// This is needed so we can later blacklist specific exports from Webpack search by making them non-enumerable
this.d = function (exports: object, getters: object) {
for (const key in getters) {
if (Object.hasOwn(getters, key) && !Object.hasOwn(exports, key)) {
Object.defineProperty(exports, key, {
enumerable: true,
configurable: true,
get: getters[key],
});
}
}
};
}; };
} }
}); });
@ -407,19 +421,8 @@ function runFactoryWithWrap(patchedFactory: PatchedModuleFactory, thisArg: unkno
return factoryReturn; return factoryReturn;
} }
if (typeof require === "function") { if (typeof require === "function" && require.c) {
const shouldIgnoreModule = _shouldIgnoreModule(exports); if (_blacklistBadModules(require.c, exports, module.id)) {
if (shouldIgnoreModule) {
if (require.c != null) {
Object.defineProperty(require.c, module.id, {
value: require.c[module.id],
enumerable: false,
configurable: true,
writable: true
});
}
return factoryReturn; return factoryReturn;
} }
} }

View file

@ -23,7 +23,7 @@ import { canonicalizeMatch } from "@utils/patches";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
import { Flux } from "./common"; import { Flux } from "./common";
import { AnyModuleFactory, ModuleExports, WebpackRequire } from "./wreq"; import { AnyModuleFactory, AnyWebpackRequire, ModuleExports, WebpackRequire } from "./wreq";
const logger = new Logger("Webpack"); const logger = new Logger("Webpack");
@ -112,18 +112,38 @@ export function _initWebpack(webpackRequire: WebpackRequire) {
// Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too // Credits to Zerebos for implementing this in BD, thus giving the idea for us to implement it too
const TypedArray = Object.getPrototypeOf(Int8Array); const TypedArray = Object.getPrototypeOf(Int8Array);
function _shouldIgnoreValue(value: any) { const PROXY_CHECK = "is this a proxy that returns values for any key?";
function shouldIgnoreValue(value: any) {
if (value == null) return true; if (value == null) return true;
if (value === window) return true; if (value === window) return true;
if (value === document || value === document.documentElement) return true; if (value === document || value === document.documentElement) return true;
if (value[Symbol.toStringTag] === "DOMTokenList") return true; if (value[Symbol.toStringTag] === "DOMTokenList" || value[Symbol.toStringTag] === "IntlMessagesProxy") return true;
// Discord might export a Proxy that returns non-null values for any property key which would pass all findByProps filters.
// One example of this is their i18n Proxy. However, that is already covered by the IntlMessagesProxy check above.
// As a fallback if they ever change the name or add a new Proxy, use a unique string to detect such proxies and ignore them
if (value[PROXY_CHECK] !== void 0) {
// their i18n Proxy "caches" by setting each accessed property to the return, so try to delete
Reflect.deleteProperty(value, PROXY_CHECK);
return true;
}
if (value instanceof TypedArray) return true; if (value instanceof TypedArray) return true;
return false; return false;
} }
export function _shouldIgnoreModule(exports: any) { function makePropertyNonEnumerable(target: Object, key: PropertyKey) {
if (_shouldIgnoreValue(exports)) { const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor == null) return;
Reflect.defineProperty(target, key, {
...descriptor,
enumerable: false
});
}
export function _blacklistBadModules(requireCache: NonNullable<AnyWebpackRequire["c"]>, exports: ModuleExports, moduleId: PropertyKey) {
if (shouldIgnoreValue(exports)) {
makePropertyNonEnumerable(requireCache, moduleId);
return true; return true;
} }
@ -131,14 +151,16 @@ export function _shouldIgnoreModule(exports: any) {
return false; return false;
} }
let allNonEnumerable = true; let hasOnlyBadProperties = true;
for (const exportKey in exports) { for (const exportKey in exports) {
if (!_shouldIgnoreValue(exports[exportKey])) { if (shouldIgnoreValue(exports[exportKey])) {
allNonEnumerable = false; makePropertyNonEnumerable(exports, exportKey);
} else {
hasOnlyBadProperties = false;
} }
} }
return allNonEnumerable; return hasOnlyBadProperties;
} }
let devToolsOpen = false; let devToolsOpen = false;
@ -406,7 +428,10 @@ export function findByCodeLazy(...code: CodeFilter) {
* Find a store by its displayName * Find a store by its displayName
*/ */
export function findStore(name: StoreNameFilter) { export function findStore(name: StoreNameFilter) {
const res: any = Flux.Store.getAll().find(filters.byStoreName(name)); const res = Flux.Store.getAll
? Flux.Store.getAll().find(filters.byStoreName(name))
: find(filters.byStoreName(name), { isIndirect: true });
if (!res) if (!res)
handleModuleNotFound("findStore", name); handleModuleNotFound("findStore", name);
return res; return res;
@ -474,12 +499,27 @@ export function findExportedComponentLazy<T extends object = any>(...props: Prop
}); });
} }
function getAllPropertyNames(object: Object, includeNonEnumerable: boolean) {
const names = new Set<PropertyKey>();
const getKeys = includeNonEnumerable ? Object.getOwnPropertyNames : Object.keys;
do {
getKeys(object).forEach(name => names.add(name));
object = Object.getPrototypeOf(object);
} while (object != null);
return names;
}
/** /**
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module) * Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
* then maps it into an easily usable module via the specified mappers. * then maps it into an easily usable module via the specified mappers.
* *
* @param code The code to look for * @param code The code to look for
* @param mappers Mappers to create the non mangled exports * @param mappers Mappers to create the non mangled exports
* @param includeBlacklistedExports Whether to include blacklisted exports in the search.
* These exports are dangerous. Accessing properties on them may throw errors
* or always return values (so a byProps filter will always return true)
* @returns Unmangled exports as specified in mappers * @returns Unmangled exports as specified in mappers
* *
* @example mapMangledModule("headerIdIsManaged:", { * @example mapMangledModule("headerIdIsManaged:", {
@ -487,7 +527,7 @@ export function findExportedComponentLazy<T extends object = any>(...props: Prop
* closeModal: filters.byCode("key==") * closeModal: filters.byCode("key==")
* }) * })
*/ */
export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> { export const mapMangledModule = traceFunction("mapMangledModule", function mapMangledModule<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {
const exports = {} as Record<S, any>; const exports = {} as Record<S, any>;
const id = findModuleId(...Array.isArray(code) ? code : [code]); const id = findModuleId(...Array.isArray(code) ? code : [code]);
@ -495,8 +535,9 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
return exports; return exports;
const mod = wreq(id as any); const mod = wreq(id as any);
const keys = getAllPropertyNames(mod, includeBlacklistedExports);
outer: outer:
for (const key in mod) { for (const key of keys) {
const member = mod[key]; const member = mod[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
@ -510,24 +551,13 @@ export const mapMangledModule = traceFunction("mapMangledModule", function mapMa
}); });
/** /**
* {@link mapMangledModule}, lazy. * lazy mapMangledModule
* @see {@link mapMangledModule}
* Finds a mangled module by the provided code "code" (must be unique and can be anywhere in the module)
* then maps it into an easily usable module via the specified mappers.
*
* @param code The code to look for
* @param mappers Mappers to create the non mangled exports
* @returns Unmangled exports as specified in mappers
*
* @example mapMangledModule("headerIdIsManaged:", {
* openModal: filters.byCode("headerIdIsManaged:"),
* closeModal: filters.byCode("key==")
* })
*/ */
export function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>): Record<S, any> { export function mapMangledModuleLazy<S extends string>(code: string | RegExp | CodeFilter, mappers: Record<S, FilterFn>, includeBlacklistedExports = false): Record<S, any> {
if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers]]); if (IS_REPORTER) lazyWebpackSearchHistory.push(["mapMangledModule", [code, mappers, includeBlacklistedExports]]);
return proxyLazy(() => mapMangledModule(code, mappers)); return proxyLazy(() => mapMangledModule(code, mappers, includeBlacklistedExports));
} }
export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/; export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("?[^)]+?"?\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"?([^)]+?)"?\)\)/;