From a055b1d47b971e92fc34cd7b13ac0e172cfd19fd Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Thu, 2 May 2024 18:52:41 -0300 Subject: [PATCH 01/16] refactor(Webpack): more reliable patching (#2237) --- scripts/generateReport.ts | 284 ++++++++++-------- src/plugins/index.ts | 49 ++- src/plugins/showHiddenChannels/index.tsx | 6 +- src/utils/patches.ts | 10 +- src/webpack/common/components.ts | 2 +- src/webpack/common/utils.ts | 3 + src/webpack/patchWebpack.ts | 363 ++++++++++++++--------- src/webpack/webpack.ts | 28 +- 8 files changed, 443 insertions(+), 302 deletions(-) diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 41e384295..912f38eda 100644 --- a/scripts/generateReport.ts +++ b/scripts/generateReport.ts @@ -269,7 +269,7 @@ page.on("pageerror", e => console.error("[Page Error]", e)); await page.setBypassCSP(true); -function runTime(token: string) { +async function runtime(token: string) { console.log("[PUP_DEBUG]", "Starting test..."); try { @@ -282,9 +282,13 @@ function runTime(token: string) { // Monkey patch Logger to not log with custom css // @ts-ignore + const originalLog = Vencord.Util.Logger.prototype._log; + // @ts-ignore Vencord.Util.Logger.prototype._log = function (level, levelColor, args) { if (level === "warn" || level === "error") - console[level]("[Vencord]", this.name + ":", ...args); + return console[level]("[Vencord]", this.name + ":", ...args); + + return originalLog.call(this, level, levelColor, args); }; // Force enable all plugins and patches @@ -310,45 +314,30 @@ function runTime(token: string) { }); }); - Vencord.Webpack.waitFor( - "loginToken", - m => { - console.log("[PUP_DEBUG]", "Logging in with token..."); - m.loginToken(token); - } - ); + let wreq: typeof Vencord.Webpack.wreq; - // Force load all chunks - Vencord.Webpack.onceReady.then(() => setTimeout(async () => { - console.log("[PUP_DEBUG]", "Webpack is ready!"); + const { canonicalizeMatch, Logger } = Vencord.Util; - const { wreq } = Vencord.Webpack; + const validChunks = new Set(); + const invalidChunks = new Set(); - console.log("[PUP_DEBUG]", "Loading all chunks..."); + let chunksSearchingResolve: (value: void | PromiseLike) => void; + const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r); - let chunks = null as Record | null; - const sym = Symbol("Vencord.chunksExtract"); + // True if resolved, false otherwise + const chunksSearchPromises = [] as Array<() => boolean>; + const lazyChunkRegex = canonicalizeMatch(/Promise\.all\((\[\i\.\i\(".+?"\).+?\])\).then\(\i\.bind\(\i,"(.+?)"\)\)/g); + const chunkIdsRegex = canonicalizeMatch(/\("(.+?)"\)/g); - Object.defineProperty(Object.prototype, sym, { - get() { - chunks = this; - }, - set() { }, - configurable: true, - }); + async function searchAndLoadLazyChunks(factoryCode: string) { + const lazyChunks = factoryCode.matchAll(lazyChunkRegex); + const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>(); - await (wreq as any).el(sym); - delete Object.prototype[sym]; + await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => { + const chunkIds = Array.from(rawChunkIds.matchAll(chunkIdsRegex)).map(m => m[1]); + if (chunkIds.length === 0) return; - const validChunksEntryPoints = new Set(); - const validChunks = new Set(); - const invalidChunks = new Set(); - - if (!chunks) throw new Error("Failed to get chunks"); - - for (const entryPoint in chunks) { - const chunkIds = chunks[entryPoint]; - let invalidEntryPoint = false; + let invalidChunkGroup = false; for (const id of chunkIds) { if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue; @@ -359,56 +348,28 @@ function runTime(token: string) { if (isWasm) { invalidChunks.add(id); - invalidEntryPoint = true; + invalidChunkGroup = true; continue; } validChunks.add(id); } - if (!invalidEntryPoint) - validChunksEntryPoints.add(entryPoint); - } + if (!invalidChunkGroup) { + validChunkGroups.add([chunkIds, entryPoint]); + } + })); - for (const entryPoint of validChunksEntryPoints) { - try { - // Loads all chunks required for an entry point - await (wreq as any).el(entryPoint); - } catch (err) { } - } + // Loads all found valid chunk groups + await Promise.all( + Array.from(validChunkGroups) + .map(([chunkIds]) => + Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { }))) + ) + ); - // Matches "id" or id: - const chunkIdRegex = /(?:"(\d+?)")|(?:(\d+?):)/g; - const wreqU = wreq.u.toString(); - - const allChunks = [] as string[]; - let currentMatch: RegExpExecArray | null; - - while ((currentMatch = chunkIdRegex.exec(wreqU)) != null) { - const id = currentMatch[1] ?? currentMatch[2]; - if (id == null) continue; - - allChunks.push(id); - } - - if (allChunks.length === 0) throw new Error("Failed to get all chunks"); - const chunksLeft = allChunks.filter(id => { - return !(validChunks.has(id) || invalidChunks.has(id)); - }); - - for (const id of chunksLeft) { - const isWasm = await fetch(wreq.p + wreq.u(id)) - .then(r => r.text()) - .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); - - // Loads a chunk - if (!isWasm) await wreq.e(id as any); - } - - // Make sure every chunk has finished loading - await new Promise(r => setTimeout(r, 1000)); - - for (const entryPoint of validChunksEntryPoints) { + // Requires the entry points for all valid chunk groups + for (const [, entryPoint] of validChunkGroups) { try { if (wreq.m[entryPoint]) wreq(entryPoint as any); } catch (err) { @@ -416,54 +377,139 @@ function runTime(token: string) { } } - console.log("[PUP_DEBUG]", "Finished loading all chunks!"); + // setImmediate to only check if all chunks were loaded after this function resolves + // We check if all chunks were loaded every time a factory is loaded + // If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved + // But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them + setTimeout(() => { + let allResolved = true; - for (const patch of Vencord.Plugins.patches) { - if (!patch.all) { - new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); - } - } + for (let i = 0; i < chunksSearchPromises.length; i++) { + const isResolved = chunksSearchPromises[i](); - for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) { - let method = searchType; - - if (searchType === "findComponent") method = "find"; - if (searchType === "findExportedComponent") method = "findByProps"; - if (searchType === "waitFor" || searchType === "waitForComponent") { - if (typeof args[0] === "string") method = "findByProps"; - else method = "find"; - } - if (searchType === "waitForStore") method = "findStore"; - - try { - let result: any; - - if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { - const [factory] = args; - result = factory(); - } else if (method === "extractAndLoadChunks") { - const [code, matcher] = args; - - const module = Vencord.Webpack.findModuleFactory(...code); - if (module) result = module.toString().match(Vencord.Util.canonicalizeMatch(matcher)); + if (isResolved) { + // Remove finished promises to avoid having to iterate through a huge array everytime + chunksSearchPromises.splice(i--, 1); } else { - // @ts-ignore - result = Vencord.Webpack[method](...args); + allResolved = false; } - - if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; - } catch (e) { - let logMessage = searchType; - if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; - else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; - else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; - - console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); } - } - setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); - }, 1000)); + if (allResolved) chunksSearchingResolve(); + }, 0); + } + + Vencord.Webpack.waitFor( + "loginToken", + m => { + console.log("[PUP_DEBUG]", "Logging in with token..."); + m.loginToken(token); + } + ); + + Vencord.Webpack.beforeInitListeners.add(async webpackRequire => { + console.log("[PUP_DEBUG]", "Loading all chunks..."); + + wreq = webpackRequire; + + Vencord.Webpack.factoryListeners.add(factory => { + let isResolved = false; + searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + }); + + // setImmediate to only search the initial factories after Discord initialized the app + // our beforeInitListeners are called before Discord initializes the app + setTimeout(() => { + for (const factoryId in wreq.m) { + let isResolved = false; + searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true); + + chunksSearchPromises.push(() => isResolved); + } + }, 0); + }); + + await chunksSearchingDone; + + // All chunks Discord has mapped to asset files, even if they are not used anymore + const allChunks = [] as string[]; + + // Matches "id" or id: + for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) { + const id = currentMatch[1] ?? currentMatch[2]; + if (id == null) continue; + + allChunks.push(id); + } + + if (allChunks.length === 0) throw new Error("Failed to get all chunks"); + + // Chunks that are not loaded (not used) by Discord code anymore + const chunksLeft = allChunks.filter(id => { + return !(validChunks.has(id) || invalidChunks.has(id)); + }); + + await Promise.all(chunksLeft.map(async id => { + const isWasm = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push")); + + // Loads and requires a chunk + if (!isWasm) { + await wreq.e(id as any); + if (wreq.m[id]) wreq(id as any); + } + })); + + console.log("[PUP_DEBUG]", "Finished loading all chunks!"); + + for (const patch of Vencord.Plugins.patches) { + if (!patch.all) { + new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); + } + } + + for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) { + let method = searchType; + + if (searchType === "findComponent") method = "find"; + if (searchType === "findExportedComponent") method = "findByProps"; + if (searchType === "waitFor" || searchType === "waitForComponent") { + if (typeof args[0] === "string") method = "findByProps"; + else method = "find"; + } + if (searchType === "waitForStore") method = "findStore"; + + try { + let result: any; + + if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") { + const [factory] = args; + result = factory(); + } else if (method === "extractAndLoadChunks") { + const [code, matcher] = args; + + const module = Vencord.Webpack.findModuleFactory(...code); + if (module) result = module.toString().match(canonicalizeMatch(matcher)); + } else { + // @ts-ignore + result = Vencord.Webpack[method](...args); + } + + if (result == null || ("$$vencordInternal" in result && result.$$vencordInternal() == null)) throw "a rock at ben shapiro"; + } catch (e) { + let logMessage = searchType; + if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`; + else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`; + else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`; + + console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage); + } + } + + setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000); } catch (e) { console.log("[PUP_DEBUG]", "A fatal error occurred:", e); process.exit(1); @@ -473,7 +519,7 @@ function runTime(token: string) { await page.evaluateOnNewDocument(` ${readFileSync("./dist/browser.js", "utf-8")} - ;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); + ;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)}); `); await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login"); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 7092001ee..2d5e3e5a4 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -34,6 +34,9 @@ export const PMLogger = logger; export const plugins = Plugins; export const patches = [] as Patch[]; +/** Whether we have subscribed to flux events of all the enabled plugins when FluxDispatcher was ready */ +let enabledPluginsSubscribedFlux = false; + const settings = Settings.plugins; export function isPluginEnabled(p: string) { @@ -119,6 +122,33 @@ export function startDependenciesRecursive(p: Plugin) { return { restartNeeded, failures }; } +export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) { + if (p.flux) { + logger.debug("Subscribing to flux events of plugin", p.name); + for (const [event, handler] of Object.entries(p.flux)) { + fluxDispatcher.subscribe(event as FluxEvents, handler); + } + } +} + +export function unsubscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) { + if (p.flux) { + logger.debug("Unsubscribing from flux events of plugin", p.name); + for (const [event, handler] of Object.entries(p.flux)) { + fluxDispatcher.unsubscribe(event as FluxEvents, handler); + } + } +} + +export function subscribeAllPluginsFluxEvents(fluxDispatcher: typeof FluxDispatcher) { + enabledPluginsSubscribedFlux = true; + + for (const name in Plugins) { + if (!isPluginEnabled(name)) continue; + subscribePluginFluxEvents(Plugins[name], fluxDispatcher); + } +} + export const startPlugin = traceFunction("startPlugin", function startPlugin(p: Plugin) { const { name, commands, flux, contextMenus } = p; @@ -138,7 +168,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } if (commands?.length) { - logger.info("Registering commands of plugin", name); + logger.debug("Registering commands of plugin", name); for (const cmd of commands) { try { registerCommand(cmd, name); @@ -149,13 +179,13 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: } } - if (flux) { - for (const event in flux) { - FluxDispatcher.subscribe(event as FluxEvents, flux[event]); - } + if (enabledPluginsSubscribedFlux) { + subscribePluginFluxEvents(p, FluxDispatcher); } + if (contextMenus) { + logger.debug("Adding context menus patches of plugin", name); for (const navId in contextMenus) { addContextMenuPatch(navId, contextMenus[navId]); } @@ -182,7 +212,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } if (commands?.length) { - logger.info("Unregistering commands of plugin", name); + logger.debug("Unregistering commands of plugin", name); for (const cmd of commands) { try { unregisterCommand(cmd.name); @@ -193,13 +223,10 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu } } - if (flux) { - for (const event in flux) { - FluxDispatcher.unsubscribe(event as FluxEvents, flux[event]); - } - } + unsubscribePluginFluxEvents(p, FluxDispatcher); if (contextMenus) { + logger.debug("Removing context menus patches of plugin", name); for (const navId in contextMenus) { removeContextMenuPatch(navId, contextMenus[navId]); } diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx index ce4a47ade..09aa2302a 100644 --- a/src/plugins/showHiddenChannels/index.tsx +++ b/src/plugins/showHiddenChannels/index.tsx @@ -36,6 +36,8 @@ const enum ShowMode { HiddenIconWithMutedStyle } +const CONNECT = 1n << 20n; + export const settings = definePluginSettings({ hideUnreads: { description: "Hide Unreads", @@ -273,12 +275,12 @@ export default definePlugin({ { // Change the role permission check to CONNECT if the channel is locked match: /ADMINISTRATOR\)\|\|(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, - replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):` + replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):` }, { // Change the permissionOverwrite check to CONNECT if the channel is locked match: /permissionOverwrites\[.+?\i=(?<=context:(\i)}.+?)(?=(.+?)VIEW_CHANNEL)/, - replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${PermissionsBits.CONNECT}n,${channel})?${permCheck}CONNECT):` + replace: (m, channel, permCheck) => `${m}!Vencord.Webpack.Common.PermissionStore.can(${CONNECT}n,${channel})?${permCheck}CONNECT):` }, { // Include the @everyone role in the allowed roles list for Hidden Channels diff --git a/src/utils/patches.ts b/src/utils/patches.ts index c30f7b17c..99f0595d6 100644 --- a/src/utils/patches.ts +++ b/src/utils/patches.ts @@ -18,20 +18,20 @@ import { PatchReplacement, ReplaceFn } from "./types"; -export function canonicalizeMatch(match: RegExp | string) { +export function canonicalizeMatch(match: T): T { if (typeof match === "string") return match; const canonSource = match.source .replaceAll("\\i", "[A-Za-z_$][\\w$]*"); - return new RegExp(canonSource, match.flags); + return new RegExp(canonSource, match.flags) as T; } -export function canonicalizeReplace(replace: string | ReplaceFn, pluginName: string): string | ReplaceFn { +export function canonicalizeReplace(replace: T, pluginName: string): T { const self = `Vencord.Plugins.plugins[${JSON.stringify(pluginName)}]`; if (typeof replace !== "function") - return replace.replaceAll("$self", self); + return replace.replaceAll("$self", self) as T; - return (...args) => replace(...args).replaceAll("$self", self); + return ((...args) => replace(...args).replaceAll("$self", self)) as T; } export function canonicalizeDescriptor(descriptor: TypedPropertyDescriptor, canonicalize: (value: T) => T) { diff --git a/src/webpack/common/components.ts b/src/webpack/common/components.ts index 020c8fc7e..46f843ce6 100644 --- a/src/webpack/common/components.ts +++ b/src/webpack/common/components.ts @@ -36,7 +36,7 @@ export let Tooltip: t.Tooltip; export let TextInput: t.TextInput; export let TextArea: t.TextArea; export let Text: t.Text; -export let Heading: t.HeadingTag; +export let Heading: t.Heading; export let Select: t.Select; export let SearchableSelect: t.SearchableSelect; export let Slider: t.Slider; diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index ec6c0e1ed..f5d30cd76 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -26,6 +26,9 @@ export let FluxDispatcher: t.FluxDispatcher; waitFor(["dispatch", "subscribe"], m => { FluxDispatcher = m; + // Non import call to avoid circular dependency + Vencord.Plugins.subscribeAllPluginsFluxEvents(m); + const cb = () => { m.unsubscribe("CONNECTION_OPEN", cb); _resolveReady(); diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index db47c875a..d2b569e8d 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -18,66 +18,120 @@ import { WEBPACK_CHUNK } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import { canonicalizeReplacement } from "@utils/patches"; +import { canonicalizeMatch, canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; +import { WebpackInstance } from "discord-types/other"; import { traceFunction } from "../debug/Tracer"; -import { _initWebpack } from "."; +import { patches } from "../plugins"; +import { _initWebpack, beforeInitListeners, factoryListeners, moduleListeners, subscriptions, wreq } from "."; + +const logger = new Logger("WebpackInterceptor", "#8caaee"); +const initCallbackRegex = canonicalizeMatch(/{return \i\(".+?"\)}/); let webpackChunk: any[]; -const logger = new Logger("WebpackInterceptor", "#8caaee"); +// Patch the window webpack chunk setter to monkey patch the push method before any chunks are pushed +// This way we can patch the factory of everything being pushed to the modules array +Object.defineProperty(window, WEBPACK_CHUNK, { + configurable: true, -if (window[WEBPACK_CHUNK]) { - logger.info(`Patching ${WEBPACK_CHUNK}.push (was already existent, likely from cache!)`); - _initWebpack(window[WEBPACK_CHUNK]); - patchPush(window[WEBPACK_CHUNK]); -} else { - Object.defineProperty(window, WEBPACK_CHUNK, { - get: () => webpackChunk, - set: v => { - if (v?.push) { - if (!v.push.$$vencordOriginal) { - logger.info(`Patching ${WEBPACK_CHUNK}.push`); - patchPush(v); + get: () => webpackChunk, + set: v => { + if (v?.push) { + if (!v.push.$$vencordOriginal) { + logger.info(`Patching ${WEBPACK_CHUNK}.push`); + patchPush(v); + + // @ts-ignore + delete window[WEBPACK_CHUNK]; + window[WEBPACK_CHUNK] = v; + } + } + + webpackChunk = v; + } +}); + +// wreq.O is the webpack onChunksLoaded function +// Discord uses it to await for all the chunks to be loaded before initializing the app +// We monkey patch it to also monkey patch the initialize app callback to get immediate access to the webpack require and run our listeners before doing it +Object.defineProperty(Function.prototype, "O", { + configurable: true, + + set(onChunksLoaded: any) { + // When using react devtools or other extensions, or even when discord loads the sentry, we may also catch their webpack here. + // This ensures we actually got the right one + // this.e (wreq.e) is the method for loading a chunk, and only the main webpack has it + if (new Error().stack?.includes("discord.com") && String(this.e).includes("Promise.all")) { + logger.info("Found main WebpackRequire.onChunksLoaded"); + + delete (Function.prototype as any).O; + + const originalOnChunksLoaded = onChunksLoaded; + onChunksLoaded = function (this: unknown, result: any, chunkIds: string[], callback: () => any, priority: number) { + if (callback != null && initCallbackRegex.test(callback.toString())) { + Object.defineProperty(this, "O", { + value: originalOnChunksLoaded, + configurable: true + }); + + const wreq = this as WebpackInstance; + + const originalCallback = callback; + callback = function (this: unknown) { + logger.info("Patched initialize app callback invoked, initializing our internal references to WebpackRequire and running beforeInitListeners"); + _initWebpack(wreq); + + for (const beforeInitListener of beforeInitListeners) { + beforeInitListener(wreq); + } + + originalCallback.apply(this, arguments as any); + }; + + callback.toString = originalCallback.toString.bind(originalCallback); + arguments[2] = callback; } - if (_initWebpack(v)) { - logger.info("Successfully initialised Vencord webpack"); - // @ts-ignore - delete window[WEBPACK_CHUNK]; - window[WEBPACK_CHUNK] = v; - } - } - webpackChunk = v; - }, - configurable: true - }); + originalOnChunksLoaded.apply(this, arguments as any); + }; - // wreq.m is the webpack module factory. - // normally, this is populated via webpackGlobal.push, which we patch below. - // However, Discord has their .m prepopulated. - // Thus, we use this hack to immediately access their wreq.m and patch all already existing factories - // - // Update: Discord now has TWO webpack instances. Their normal one and sentry - // Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules - Object.defineProperty(Function.prototype, "m", { - set(v: any) { - // When using react devtools or other extensions, we may also catch their webpack here. - // This ensures we actually got the right one - if (new Error().stack?.includes("discord.com")) { - logger.info("Found webpack module factory"); - patchFactories(v); - } + onChunksLoaded.toString = originalOnChunksLoaded.toString.bind(originalOnChunksLoaded); + } - Object.defineProperty(this, "m", { - value: v, - configurable: true, - }); - }, - configurable: true - }); -} + Object.defineProperty(this, "O", { + value: onChunksLoaded, + configurable: true + }); + } +}); + +// wreq.m is the webpack module factory. +// normally, this is populated via webpackGlobal.push, which we patch below. +// However, Discord has their .m prepopulated. +// Thus, we use this hack to immediately access their wreq.m and patch all already existing factories +// +// Update: Discord now has TWO webpack instances. Their normal one and sentry +// Sentry does not push chunks to the global at all, so this same patch now also handles their sentry modules +Object.defineProperty(Function.prototype, "m", { + configurable: true, + + set(v: any) { + // When using react devtools or other extensions, we may also catch their webpack here. + // This ensures we actually got the right one + const error = new Error(); + if (error.stack?.includes("discord.com")) { + logger.info("Found Webpack module factory", error.stack.match(/\/assets\/(.+?\.js)/)?.[1] ?? ""); + patchFactories(v); + } + + Object.defineProperty(this, "m", { + value: v, + configurable: true + }); + } +}); function patchPush(webpackGlobal: any) { function handlePush(chunk: any) { @@ -91,6 +145,7 @@ function patchPush(webpackGlobal: any) { } handlePush.$$vencordOriginal = webpackGlobal.push; + handlePush.toString = handlePush.$$vencordOriginal.toString.bind(handlePush.$$vencordOriginal); // Webpack overwrites .push with its own push like so: `d.push = n.bind(null, d.push.bind(d));` // it wraps the old push (`d.push.bind(d)`). this old push is in this case our handlePush. // If we then repatched the new push, we would end up with recursive patching, which leads to our patches @@ -99,41 +154,41 @@ function patchPush(webpackGlobal: any) { handlePush.bind = (...args: unknown[]) => handlePush.$$vencordOriginal.bind(...args); Object.defineProperty(webpackGlobal, "push", { + configurable: true, + get: () => handlePush, set(v) { handlePush.$$vencordOriginal = v; - }, - configurable: true + } }); } -function patchFactories(factories: Record void>) { - const { subscriptions, listeners } = Vencord.Webpack; - const { patches } = Vencord.Plugins; +let webpackNotInitializedLogged = false; +function patchFactories(factories: Record void>) { for (const id in factories) { let mod = factories[id]; - // Discords Webpack chunks for some ungodly reason contain random - // newlines. Cyn recommended this workaround and it seems to work fine, - // however this could potentially break code, so if anything goes weird, - // this is probably why. - // Additionally, `[actual newline]` is one less char than "\n", so if Discord - // ever targets newer browsers, the minifier could potentially use this trick and - // cause issues. - // - // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, - let code: string = "0," + mod.toString().replaceAll("\n", ""); + const originalMod = mod; const patchedBy = new Set(); - const factory = factories[id] = function (module, exports, require) { + const factory = factories[id] = function (module: any, exports: any, require: WebpackInstance) { + if (wreq == null && IS_DEV) { + if (!webpackNotInitializedLogged) { + webpackNotInitializedLogged = true; + logger.error("WebpackRequire was not initialized, running modules without patches instead."); + } + + return void originalMod(module, exports, require); + } + try { mod(module, exports, require); } catch (err) { // Just rethrow discord errors if (mod === originalMod) throw err; - logger.error("Error in patched chunk", err); + logger.error("Error in patched module", err); return void originalMod(module, exports, require); } @@ -153,11 +208,11 @@ function patchFactories(factories: Record string, original: any, (...args: any[]): void; }; - // for some reason throws some error on which calling .toString() leads to infinite recursion - // when you force load all chunks??? - factory.toString = () => mod.toString(); + factory.toString = originalMod.toString.bind(originalMod); factory.original = originalMod; + for (const factoryListener of factoryListeners) { + try { + factoryListener(originalMod); + } catch (err) { + logger.error("Error in Webpack factory listener:\n", err, factoryListener); + } + } + + // Discords Webpack chunks for some ungodly reason contain random + // newlines. Cyn recommended this workaround and it seems to work fine, + // however this could potentially break code, so if anything goes weird, + // this is probably why. + // Additionally, `[actual newline]` is one less char than "\n", so if Discord + // ever targets newer browsers, the minifier could potentially use this trick and + // cause issues. + // + // 0, prefix is to turn it into an expression: 0,function(){} would be invalid syntax without the 0, + let code: string = "0," + mod.toString().replaceAll("\n", ""); + for (let i = 0; i < patches.length; i++) { const patch = patches[i]; - const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); if (patch.predicate && !patch.predicate()) continue; + if (!code.includes(patch.find)) continue; - if (code.includes(patch.find)) { - patchedBy.add(patch.plugin); + patchedBy.add(patch.plugin); - const previousMod = mod; - const previousCode = code; + const executePatch = traceFunction(`patch by ${patch.plugin}`, (match: string | RegExp, replace: string) => code.replace(match, replace)); + const previousMod = mod; + const previousCode = code; - // we change all patch.replacement to array in plugins/index - for (const replacement of patch.replacement as PatchReplacement[]) { - if (replacement.predicate && !replacement.predicate()) continue; - const lastMod = mod; - const lastCode = code; + // We change all patch.replacement to array in plugins/index + for (const replacement of patch.replacement as PatchReplacement[]) { + if (replacement.predicate && !replacement.predicate()) continue; - canonicalizeReplacement(replacement, patch.plugin); + const lastMod = mod; + const lastCode = code; - try { - const newCode = executePatch(replacement.match, replacement.replace as string); - if (newCode === code) { - if (!patch.noWarn) { - logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); - if (IS_DEV) { - logger.debug("Function Source:\n", code); - } + canonicalizeReplacement(replacement, patch.plugin); + + try { + const newCode = executePatch(replacement.match, replacement.replace as string); + if (newCode === code) { + if (!patch.noWarn) { + logger.warn(`Patch by ${patch.plugin} had no effect (Module id is ${id}): ${replacement.match}`); + if (IS_DEV) { + logger.debug("Function Source:\n", code); } - - if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); - code = previousCode; - mod = previousMod; - patchedBy.delete(patch.plugin); - break; - } - } else { - code = newCode; - mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); - } - } catch (err) { - logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); - - if (IS_DEV) { - const changeSize = code.length - lastCode.length; - const match = lastCode.match(replacement.match)!; - - // Use 200 surrounding characters of context - const start = Math.max(0, match.index! - 200); - const end = Math.min(lastCode.length, match.index! + match[0].length + 200); - // (changeSize may be negative) - const endPatched = end + changeSize; - - const context = lastCode.slice(start, end); - const patchedContext = code.slice(start, endPatched); - - // inline require to avoid including it in !IS_DEV builds - const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); - let fmt = "%c %s "; - const elements = [] as string[]; - for (const d of diff) { - const color = d.removed - ? "red" - : d.added - ? "lime" - : "grey"; - fmt += "%c%s"; - elements.push("color:" + color, d.value); - } - - logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); - logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); - const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); - logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); } - patchedBy.delete(patch.plugin); if (patch.group) { - logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); - code = previousCode; + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} had no effect`); mod = previousMod; + code = previousCode; + patchedBy.delete(patch.plugin); break; } - code = lastCode; - mod = lastMod; + continue; } - } - if (!patch.all) patches.splice(i--, 1); + code = newCode; + mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); + } catch (err) { + logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); + + if (IS_DEV) { + const changeSize = code.length - lastCode.length; + const match = lastCode.match(replacement.match)!; + + // Use 200 surrounding characters of context + const start = Math.max(0, match.index! - 200); + const end = Math.min(lastCode.length, match.index! + match[0].length + 200); + // (changeSize may be negative) + const endPatched = end + changeSize; + + const context = lastCode.slice(start, end); + const patchedContext = code.slice(start, endPatched); + + // inline require to avoid including it in !IS_DEV builds + const diff = (require("diff") as typeof import("diff")).diffWordsWithSpace(context, patchedContext); + let fmt = "%c %s "; + const elements = [] as string[]; + for (const d of diff) { + const color = d.removed + ? "red" + : d.added + ? "lime" + : "grey"; + fmt += "%c%s"; + elements.push("color:" + color, d.value); + } + + logger.errorCustomFmt(...Logger.makeTitle("white", "Before"), context); + logger.errorCustomFmt(...Logger.makeTitle("white", "After"), patchedContext); + const [titleFmt, ...titleElements] = Logger.makeTitle("white", "Diff"); + logger.errorCustomFmt(titleFmt + fmt, ...titleElements, ...elements); + } + + patchedBy.delete(patch.plugin); + + if (patch.group) { + logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); + mod = previousMod; + code = previousCode; + break; + } + + mod = lastMod; + code = lastCode; + } } + + if (!patch.all) patches.splice(i--, 1); } } } diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 564da4813..34a9b69c9 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -68,20 +68,16 @@ export const filters = { } }; -export const subscriptions = new Map(); -export const listeners = new Set(); - export type CallbackFn = (mod: any, id: string) => void; -export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { - if (cache !== void 0) throw "no."; +export const subscriptions = new Map(); +export const moduleListeners = new Set(); +export const factoryListeners = new Set<(factory: (module: any, exports: any, require: WebpackInstance) => void) => void>(); +export const beforeInitListeners = new Set<(wreq: WebpackInstance) => void>(); - instance.push([[Symbol("Vencord")], {}, r => wreq = r]); - instance.pop(); - if (!wreq) return false; - - cache = wreq.c; - return true; +export function _initWebpack(webpackRequire: WebpackInstance) { + wreq = webpackRequire; + cache = webpackRequire.c; } let devToolsOpen = false; @@ -425,7 +421,7 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def const match = module.toString().match(canonicalizeMatch(matcher)); if (!match) { - const err = new Error("extractAndLoadChunks: Couldn't find entry point id in module factory code"); + const err = new Error("extractAndLoadChunks: Couldn't find chunk loading in module factory code"); logger.warn(err, "Code:", code, "Matcher:", matcher); // Strict behaviour in DevBuilds to fail early and make sure the issue is found @@ -491,14 +487,6 @@ export function waitFor(filter: string | string[] | FilterFn, callback: Callback subscriptions.set(filter, callback); } -export function addListener(callback: CallbackFn) { - listeners.add(callback); -} - -export function removeListener(callback: CallbackFn) { - listeners.delete(callback); -} - /** * Search modules by keyword. This searches the factory methods, * meaning you can search all sorts of things, displayName, methodName, strings somewhere in the code, etc From 85d6d74a3e14ed4a1cc83c8131b83c08cfdce04a Mon Sep 17 00:00:00 2001 From: HAHALOSAH <67280050+HAHALOSAH@users.noreply.github.com> Date: Thu, 2 May 2024 23:00:00 +0000 Subject: [PATCH 02/16] messageLogger: fix niche bug ignoring edits when content is same (#2403) --- src/plugins/messageLogger/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/messageLogger/index.tsx b/src/plugins/messageLogger/index.tsx index c3a25e1b6..988cd8e39 100644 --- a/src/plugins/messageLogger/index.tsx +++ b/src/plugins/messageLogger/index.tsx @@ -255,7 +255,7 @@ export default definePlugin({ replace: "$1" + ".update($3,m =>" + " (($2.message.flags & 64) === 64 || $self.shouldIgnore($2.message, true)) ? m :" + - " $2.message.content !== m.editHistory?.[0]?.content && $2.message.content !== m.content ?" + + " $2.message.content !== m.content ?" + " m.set('editHistory',[...(m.editHistory || []), $self.makeEdit($2.message, m)]) :" + " m" + ")" + From 86b53b24a66048c5b961f43575611fcdcb83b753 Mon Sep 17 00:00:00 2001 From: dolfies Date: Thu, 2 May 2024 19:09:53 -0400 Subject: [PATCH 03/16] feat(plugin): PauseInvitesForever (#2372) Co-authored-by: V --- src/plugins/pauseInvitesForever/README.md | 5 ++ src/plugins/pauseInvitesForever/index.tsx | 74 +++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/plugins/pauseInvitesForever/README.md create mode 100644 src/plugins/pauseInvitesForever/index.tsx diff --git a/src/plugins/pauseInvitesForever/README.md b/src/plugins/pauseInvitesForever/README.md new file mode 100644 index 000000000..7ccf0b72f --- /dev/null +++ b/src/plugins/pauseInvitesForever/README.md @@ -0,0 +1,5 @@ +# PauseInvitesForever + +Adds a button to the Security Actions modal to to pause invites indefinitely. + +![](https://github.com/Vendicated/Vencord/assets/47677887/e5ba40a3-cb08-462a-8615-fb74dd54c790) diff --git a/src/plugins/pauseInvitesForever/index.tsx b/src/plugins/pauseInvitesForever/index.tsx new file mode 100644 index 000000000..81f18fd6e --- /dev/null +++ b/src/plugins/pauseInvitesForever/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 . +*/ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; +import { findByPropsLazy } from "@webpack"; +import { GuildStore, RestAPI } from "@webpack/common"; + +const Messages = findByPropsLazy("GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION"); +const { InvitesDisabledExperiment } = findByPropsLazy("InvitesDisabledExperiment"); + +export default definePlugin({ + name: "PauseInvitesForever", + tags: ["DisableInvitesForever"], + description: "Brings back the option to pause invites indefinitely that stupit Discord removed.", + authors: [Devs.Dolfies, Devs.amia], + + patches: [ + { + find: "Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION", + replacement: [{ + match: /children:\i\.\i\.\i\.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION/, + replace: "children: $self.renderInvitesLabel(arguments[0].guildId, setChecked)", + }, + { + match: /(\i\.hasDMsDisabled\)\(\i\),\[\i,(\i)\]=\i\.useState\(\i\))/, + replace: "$1,setChecked=$2" + }] + } + ], + + showDisableInvites(guildId: string) { + // Once the experiment is removed, this should keep working + const { enableInvitesDisabled } = InvitesDisabledExperiment?.getCurrentConfig?.({ guildId }) ?? { enableInvitesDisabled: true }; + // @ts-ignore + return enableInvitesDisabled && !GuildStore.getGuild(guildId).hasFeature("INVITES_DISABLED"); + }, + + disableInvites(guildId: string) { + const guild = GuildStore.getGuild(guildId); + const features = [...guild.features, "INVITES_DISABLED"]; + RestAPI.patch({ + url: `/guilds/${guild.id}`, + body: { features }, + }); + }, + + renderInvitesLabel(guildId: string, setChecked: Function) { + return ( +
+ {Messages.GUILD_INVITE_DISABLE_ACTION_SHEET_DESCRIPTION} + {this.showDisableInvites(guildId) && { + setChecked(true); + this.disableInvites(guildId); + }}> Pause Indefinitely.} +
+ ); + } +}); From 84c53b4a27404059446f3748af32cb4da8d2bac0 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 3 May 2024 04:52:07 -0300 Subject: [PATCH 04/16] MoreUserTags: Remove old workaround; MessageClickActions: Move finds outside of start --- src/plugins/messageClickActions/index.ts | 7 +++---- src/plugins/moreUserTags/index.tsx | 12 +++++------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index 052c33f61..3d4bb21bb 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -23,11 +23,13 @@ import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; import { FluxDispatcher, PermissionsBits, PermissionStore, UserStore } from "@webpack/common"; +const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); +const EditStore = findByPropsLazy("isEditing", "isEditingAny"); + let isDeletePressed = false; const keydown = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = true); const keyup = (e: KeyboardEvent) => e.key === "Backspace" && (isDeletePressed = false); - const settings = definePluginSettings({ enableDeleteOnClick: { type: OptionType.BOOLEAN, @@ -60,9 +62,6 @@ export default definePlugin({ settings, start() { - const MessageActions = findByPropsLazy("deleteMessage", "startEditMessage"); - const EditStore = findByPropsLazy("isEditing", "isEditingAny"); - document.addEventListener("keydown", keydown); document.addEventListener("keyup", keyup); diff --git a/src/plugins/moreUserTags/index.tsx b/src/plugins/moreUserTags/index.tsx index 4ac542216..df47e5456 100644 --- a/src/plugins/moreUserTags/index.tsx +++ b/src/plugins/moreUserTags/index.tsx @@ -198,8 +198,7 @@ export default definePlugin({ replacement: [ // make the tag show the right text { - // FIXME: Remove the BOT_TAG_BOT variant when the change arrives in stable - match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.(?:APP_TAG|BOT_TAG_BOT)/, + match: /(switch\((\i)\){.+?)case (\i(?:\.\i)?)\.BOT:default:(\i)=.{0,40}(\i\.\i\.Messages)\.APP_TAG/, replace: (_, origSwitch, variant, tags, displayedText, strings) => `${origSwitch}default:{${displayedText} = $self.getTagText(${tags}[${variant}], ${strings})}` }, @@ -322,20 +321,19 @@ export default definePlugin({ isOPTag: (tag: number) => tag === Tag.Types.ORIGINAL_POSTER || tags.some(t => tag === Tag.Types[`${t.name}-OP`]), - // FIXME: Remove the BOT_TAG_BOT variants from strings when the change arrives in stable getTagText(passedTagName: string, strings: Record) { - if (!passedTagName) return strings.APP_TAG ?? strings.BOT_TAG_BOT; + if (!passedTagName) return strings.APP_TAG; const [tagName, variant] = passedTagName.split("-"); const tag = tags.find(({ name }) => tagName === name); - if (!tag) return strings.APP_TAG ?? strings.BOT_TAG_BOT; - if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG ?? strings.BOT_TAG_BOT; + if (!tag) return strings.APP_TAG; + if (variant === "BOT" && tagName !== "WEBHOOK" && this.settings.store.dontShowForBots) return strings.APP_TAG; const tagText = settings.store.tagSettings?.[tag.name]?.text || tag.displayName; switch (variant) { case "OP": return `${strings.BOT_TAG_FORUM_ORIGINAL_POSTER} • ${tagText}`; case "BOT": - return `${strings.APP_TAG ?? strings.BOT_TAG_BOT} • ${tagText}`; + return `${strings.APP_TAG} • ${tagText}`; default: return tagText; } From 03d7e0fb93a32c2ee6984cfdb7e2d5ea3f4de127 Mon Sep 17 00:00:00 2001 From: dolfies Date: Fri, 3 May 2024 09:09:54 -0400 Subject: [PATCH 05/16] fix sort conflict of ImplicitRelationships & SortFriendRequests (#2408) --- src/plugins/implicitRelationships/index.ts | 8 ++++---- src/plugins/sortFriendRequests/index.tsx | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/plugins/implicitRelationships/index.ts b/src/plugins/implicitRelationships/index.ts index be17e0fe4..15165d64a 100644 --- a/src/plugins/implicitRelationships/index.ts +++ b/src/plugins/implicitRelationships/index.ts @@ -81,8 +81,8 @@ export default definePlugin({ find: "getRelationshipCounts(){", replacement: { predicate: () => Settings.plugins.ImplicitRelationships.sortByAffinity, - match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: "$&.sortBy((row) => $self.sortList(row))" + match: /\}\)\.sortBy\((.+?)\)\.value\(\)/, + replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()" } }, @@ -120,10 +120,10 @@ export default definePlugin({ } ), - sortList(row: any) { + wrapSort(comparator: Function, row: any) { return row.type === 5 ? -UserAffinitiesStore.getUserAffinity(row.user.id)?.affinity ?? 0 - : row.comparator; + : comparator(row); }, async fetchImplicitRelationships() { diff --git a/src/plugins/sortFriendRequests/index.tsx b/src/plugins/sortFriendRequests/index.tsx index 32579a803..7033591bd 100644 --- a/src/plugins/sortFriendRequests/index.tsx +++ b/src/plugins/sortFriendRequests/index.tsx @@ -41,8 +41,8 @@ export default definePlugin({ patches: [{ find: "getRelationshipCounts(){", replacement: { - match: /\.sortBy\(\i=>\i\.comparator\)/, - replace: "$&.sortBy((row) => $self.sortList(row))" + match: /\}\)\.sortBy\((.+?)\)\.value\(\)/, + replace: "}).sortBy(row => $self.wrapSort(($1), row)).value()" } }, { find: ".Messages.FRIEND_REQUEST_CANCEL", @@ -53,10 +53,10 @@ export default definePlugin({ } }], - sortList(row: any) { + wrapSort(comparator: Function, row: any) { return row.type === 3 || row.type === 4 ? -this.getSince(row.user) - : row.comparator; + : comparator(row); }, getSince(user: User) { From 315f4f4e587f1d15f69b94c0d1b89b42f43c5eaf Mon Sep 17 00:00:00 2001 From: Sqaaakoi Date: Sat, 4 May 2024 01:17:12 +1200 Subject: [PATCH 06/16] ReviewDB: add more context menu shortcuts to view reviews (#2382) Co-authored-by: V --- src/plugins/reviewDB/index.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/plugins/reviewDB/index.tsx b/src/plugins/reviewDB/index.tsx index 0c3578b07..bcd0f163f 100644 --- a/src/plugins/reviewDB/index.tsx +++ b/src/plugins/reviewDB/index.tsx @@ -35,13 +35,25 @@ import { getCurrentUserInfo, readNotification } from "./reviewDbApi"; import { settings } from "./settings"; import { showToast } from "./utils"; -const guildPopoutPatch: NavContextMenuPatchCallback = (children, props: { guild: Guild, onClose(): void; }) => { +const guildPopoutPatch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild, onClose(): void; }) => { children.push( openReviewsModal(props.guild.id, props.guild.name)} + action={() => openReviewsModal(guild.id, guild.name)} + /> + ); +}; + +const userContextPatch: NavContextMenuPatchCallback = (children, { user }: { user?: User, onClose(): void; }) => { + if (!user) return; + children.push( + openReviewsModal(user.id, user.username)} /> ); }; @@ -53,7 +65,10 @@ export default definePlugin({ settings, contextMenus: { - "guild-header-popout": guildPopoutPatch + "guild-header-popout": guildPopoutPatch, + "guild-context": guildPopoutPatch, + "user-context": userContextPatch, + "user-profile-actions": userContextPatch }, patches: [ From 1af44b25f3fa3c6be7ddf66eea82c1adbc087a5a Mon Sep 17 00:00:00 2001 From: katlyn <17418611+katlyn@users.noreply.github.com> Date: Fri, 3 May 2024 06:51:53 -0800 Subject: [PATCH 07/16] feat(USRBG): update to new API (#2388) --- src/plugins/usrbg/index.tsx | 41 +++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx index ce6bd709b..b92839a9a 100644 --- a/src/plugins/usrbg/index.tsx +++ b/src/plugins/usrbg/index.tsx @@ -24,9 +24,14 @@ import definePlugin, { OptionType } from "@utils/types"; import style from "./index.css?managed"; -const BASE_URL = "https://raw.githubusercontent.com/AutumnVN/usrbg/main/usrbg.json"; +const API_URL = "https://usrbg.is-hardly.online/users"; -let data = {} as Record; +interface UsrbgApiReturn { + endpoint: string + bucket: string + prefix: string + users: Record +} const settings = definePluginSettings({ nitroFirst: { @@ -48,7 +53,7 @@ const settings = definePluginSettings({ export default definePlugin({ name: "USRBG", description: "Displays user banners from USRBG, allowing anyone to get a banner without Nitro", - authors: [Devs.AutumnVN, Devs.pylix, Devs.TheKodeToad], + authors: [Devs.AutumnVN, Devs.katlyn, Devs.pylix, Devs.TheKodeToad], settings, patches: [ { @@ -80,8 +85,7 @@ export default definePlugin({ } ], - - data, + data: null as UsrbgApiReturn | null, settingsAboutComponent: () => { return ( @@ -91,9 +95,9 @@ export default definePlugin({ voiceBackgroundHook({ className, participantUserId }: any) { if (className.includes("tile_")) { - if (data[participantUserId]) { + if (this.userHasBackground(participantUserId)) { return { - backgroundImage: `url(${data[participantUserId]})`, + backgroundImage: `url(${this.getImageUrl(participantUserId)})`, backgroundSize: "cover", backgroundPosition: "center", backgroundRepeat: "no-repeat" @@ -104,24 +108,35 @@ export default definePlugin({ useBannerHook({ displayProfile, user }: any) { if (displayProfile?.banner && settings.store.nitroFirst) return; - if (data[user.id]) return data[user.id]; + if (this.userHasBackground(user.id)) return this.getImageUrl(user.id); }, premiumHook({ userId }: any) { - if (data[userId]) return 2; + if (this.userHasBackground(userId)) return 2; }, shouldShowBadge({ displayProfile, user }: any) { - return displayProfile?.banner && (!data[user.id] || settings.store.nitroFirst); + return displayProfile?.banner && (!this.userHasBackground(user.id) || settings.store.nitroFirst); + }, + + userHasBackground(userId: string) { + return !!this.data?.users[userId]; + }, + + getImageUrl(userId: string): string|null { + if (!this.userHasBackground(userId)) return null; + + // We can assert that data exists because userHasBackground returned true + const { endpoint, bucket, prefix, users: { [userId]: etag } } = this.data!; + return `${endpoint}/${bucket}/${prefix}${userId}?${etag}`; }, async start() { enableStyle(style); - const res = await fetch(BASE_URL); + const res = await fetch(API_URL); if (res.ok) { - data = await res.json(); - this.data = data; + this.data = await res.json(); } } }); From 78183eb226c360dbdd978c32ea08ccc9ac38f475 Mon Sep 17 00:00:00 2001 From: sunnie <78964224+sunnniee@users.noreply.github.com> Date: Fri, 3 May 2024 23:00:42 +0300 Subject: [PATCH 08/16] MsgClickActions: control ping via shift & NoReplyMention plugin (#2390) Co-authored-by: V --- src/plugins/messageClickActions/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plugins/messageClickActions/index.ts b/src/plugins/messageClickActions/index.ts index 3d4bb21bb..14899c367 100644 --- a/src/plugins/messageClickActions/index.ts +++ b/src/plugins/messageClickActions/index.ts @@ -17,7 +17,7 @@ */ import { addClickListener, removeClickListener } from "@api/MessageEvents"; -import { definePluginSettings, Settings } from "@api/Settings"; +import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; @@ -84,11 +84,17 @@ export default definePlugin({ const EPHEMERAL = 64; if (msg.hasFlag(EPHEMERAL)) return; + const isShiftPress = event.shiftKey && !settings.store.requireModifier; + const NoReplyMention = Vencord.Plugins.plugins.NoReplyMention as any as typeof import("../noReplyMention").default; + const shouldMention = Vencord.Plugins.isPluginEnabled("NoReplyMention") + ? NoReplyMention.shouldMention(msg, isShiftPress) + : !isShiftPress; + FluxDispatcher.dispatch({ type: "CREATE_PENDING_REPLY", channel, message: msg, - shouldMention: !Settings.plugins.NoReplyMention.enabled, + shouldMention, showMentionToggle: channel.guild_id !== null }); } From 520e9151681db577007ab8a5b6a1ecb5b9aae7b8 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 3 May 2024 22:18:31 +0200 Subject: [PATCH 09/16] fix badges with custom component --- src/plugins/_api/badges.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/_api/badges.tsx b/src/plugins/_api/badges.tsx index 6cbe1feb0..3cfbb4d99 100644 --- a/src/plugins/_api/badges.tsx +++ b/src/plugins/_api/badges.tsx @@ -78,7 +78,7 @@ export default definePlugin({ }, // replace their component with ours if applicable { - match: /(?<=text:(\i)\.description,spacing:12,)children:/, + match: /(?<=text:(\i)\.description,spacing:12,.{0,50})children:/, replace: "children:$1.component ? () => $self.renderBadgeComponent($1) :" }, // conditionally override their onClick with badge.onClick if it exists From 0350db76908d847d14a863fbd7bdf8f282cf6395 Mon Sep 17 00:00:00 2001 From: Kyuuhachi <1547062+Kyuuhachi@users.noreply.github.com> Date: Fri, 3 May 2024 22:42:14 +0200 Subject: [PATCH 10/16] feat(plugin): ImageLink (#2297) --- src/plugins/imageLink/README.md | 3 +++ src/plugins/imageLink/index.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/plugins/imageLink/README.md create mode 100644 src/plugins/imageLink/index.ts diff --git a/src/plugins/imageLink/README.md b/src/plugins/imageLink/README.md new file mode 100644 index 000000000..add657948 --- /dev/null +++ b/src/plugins/imageLink/README.md @@ -0,0 +1,3 @@ +# ImageLink + +If a message consists of only a link to an image, Discord hides the link and shows only the image embed. This plugin makes the link show regardless. diff --git a/src/plugins/imageLink/index.ts b/src/plugins/imageLink/index.ts new file mode 100644 index 000000000..73a977fa9 --- /dev/null +++ b/src/plugins/imageLink/index.ts @@ -0,0 +1,24 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "ImageLink", + description: "Never hide image links in messages, even if it's the only content", + authors: [Devs.Kyuuhachi], + + patches: [ + { + find: "isEmbedInline:function()", + replacement: { + match: /(?<=isEmbedInline:function\(\)\{return )\i(?=\})/, + replace: "()=>false", + }, + }, + ], +}); From 1ef87361f2f4c9117c45aab321a8c8c236e4f10d Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 3 May 2024 18:47:15 -0300 Subject: [PATCH 11/16] Test Patches: Faster chromium setup; Update action versions (#2412) --- .github/workflows/reportBrokenPlugins.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 4b09463e0..12cbb0770 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -11,20 +11,20 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: ${{ github.event_name == 'schedule' }} with: ref: dev - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: ${{ github.event_name == 'workflow_dispatch' }} - - uses: pnpm/action-setup@v2 # Install pnpm using packageManager key in package.json + - uses: pnpm/action-setup@v3 # Install pnpm using packageManager key in package.json - - name: Use Node.js 19 - uses: actions/setup-node@v3 + - name: Use Node.js 18 + uses: actions/setup-node@v4 with: - node-version: 19 + node-version: 18 cache: "pnpm" - name: Install dependencies @@ -32,7 +32,11 @@ jobs: pnpm install --frozen-lockfile pnpm add puppeteer - sudo apt-get install -y chromium-browser + - name: Install Google Chrome + id: setup-chrome + uses: browser-actions/setup-chrome@82b9ce628cc5595478a9ebadc480958a36457dc2 + with: + chrome-version: stable - name: Build web run: pnpm buildWeb --standalone --dev @@ -41,7 +45,7 @@ jobs: timeout-minutes: 10 run: | export PATH="$PWD/node_modules/.bin:$PATH" - export CHROMIUM_BIN=$(which chromium-browser) + export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }} esbuild scripts/generateReport.ts > dist/report.mjs node dist/report.mjs >> $GITHUB_STEP_SUMMARY @@ -54,7 +58,7 @@ jobs: if: success() || failure() # even run if previous one failed run: | export PATH="$PWD/node_modules/.bin:$PATH" - export CHROMIUM_BIN=$(which chromium-browser) + export CHROMIUM_BIN=${{ steps.setup-chrome.outputs.chrome-path }} export USE_CANARY=true esbuild scripts/generateReport.ts > dist/report.mjs From 54bb7b96e90ebaee308aebe578122591a27f0727 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 3 May 2024 18:58:56 -0300 Subject: [PATCH 12/16] Test Patches: Remove pnpm add puppeteer as it's already in deps --- .github/workflows/reportBrokenPlugins.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml index 12cbb0770..39d639988 100644 --- a/.github/workflows/reportBrokenPlugins.yml +++ b/.github/workflows/reportBrokenPlugins.yml @@ -30,7 +30,6 @@ jobs: - name: Install dependencies run: | pnpm install --frozen-lockfile - pnpm add puppeteer - name: Install Google Chrome id: setup-chrome From a98f12bd1eabd0746834225c4ba3e6c755fe5fd1 Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Fri, 3 May 2024 19:07:43 -0300 Subject: [PATCH 13/16] SecretRingToneEnabler: Fix patch --- src/plugins/secretRingTone/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts index 8f0176f88..9c3956a80 100644 --- a/src/plugins/secretRingTone/index.ts +++ b/src/plugins/secretRingTone/index.ts @@ -14,10 +14,11 @@ export default definePlugin({ authors: [Devs.AndrewDLO, Devs.FieryFlames], patches: [ { - find: "call_ringing_beat\"", + find: '"call_ringing_beat"', replacement: { - match: /500===\i\(\)\.random\(1,1e3\)/, - replace: "true" + // FIXME Remove === alternative when it hits stable + match: /500(!==|===)\i\(\)\.random\(1,1e3\)/, + replace: (_, predicate) => predicate === "!==" ? "false" : "true", } }, ], From a6c09bc9099832c88a83939bee776fc7d21f1f06 Mon Sep 17 00:00:00 2001 From: dolfies Date: Fri, 3 May 2024 18:21:02 -0400 Subject: [PATCH 14/16] feat(ValidUser): also display badges & banner (#2235) Co-authored-by: V --- src/plugins/validUser/index.tsx | 72 +++++++++++++++++++++++++++++++-- src/webpack/common/utils.ts | 3 +- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/plugins/validUser/index.tsx b/src/plugins/validUser/index.tsx index 2fce693e8..7a21ac86b 100644 --- a/src/plugins/validUser/index.tsx +++ b/src/plugins/validUser/index.tsx @@ -21,12 +21,37 @@ import { Devs } from "@utils/constants"; import { sleep } from "@utils/misc"; import { Queue } from "@utils/Queue"; import definePlugin from "@utils/types"; -import { UserStore, UserUtils, useState } from "@webpack/common"; +import { Constants, FluxDispatcher, RestAPI, UserProfileStore, UserStore, useState } from "@webpack/common"; import type { ComponentType, ReactNode } from "react"; +// LYING to the type checker here +const UserFlags = Constants.UserFlags as Record; +const badges: Record = { + "active_developer": { id: "active_developer", description: "Active Developer", icon: "6bdc42827a38498929a4920da12695d9", link: "https://support-dev.discord.com/hc/en-us/articles/10113997751447" }, + "bug_hunter_level_1": { id: "bug_hunter_level_1", description: "Discord Bug Hunter", icon: "2717692c7dca7289b35297368a940dd0", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" }, + "bug_hunter_level_2": { id: "bug_hunter_level_2", description: "Discord Bug Hunter", icon: "848f79194d4be5ff5f81505cbd0ce1e6", link: "https://support.discord.com/hc/en-us/articles/360046057772-Discord-Bugs" }, + "certified_moderator": { id: "certified_moderator", description: "Moderator Programs Alumni", icon: "fee1624003e2fee35cb398e125dc479b", link: "https://discord.com/safety" }, + "discord_employee": { id: "staff", description: "Discord Staff", icon: "5e74e9b61934fc1f67c65515d1f7e60d", link: "https://discord.com/company" }, + "hypesquad": { id: "hypesquad", description: "HypeSquad Events", icon: "bf01d1073931f921909045f3a39fd264", link: "https://discord.com/hypesquad" }, + "hypesquad_online_house_1": { id: "hypesquad_house_1", description: "HypeSquad Bravery", icon: "8a88d63823d8a71cd5e390baa45efa02", link: "https://discord.com/settings/hypesquad-online" }, + "hypesquad_online_house_2": { id: "hypesquad_house_2", description: "HypeSquad Brilliance", icon: "011940fd013da3f7fb926e4a1cd2e618", link: "https://discord.com/settings/hypesquad-online" }, + "hypesquad_online_house_3": { id: "hypesquad_house_3", description: "HypeSquad Balance", icon: "3aa41de486fa12454c3761e8e223442e", link: "https://discord.com/settings/hypesquad-online" }, + "partner": { id: "partner", description: "Partnered Server Owner", icon: "3f9748e53446a137a052f3454e2de41e", link: "https://discord.com/partners" }, + "premium": { id: "premium", description: "Subscriber", icon: "2ba85e8026a8614b640c2837bcdfe21b", link: "https://discord.com/settings/premium" }, + "premium_early_supporter": { id: "early_supporter", description: "Early Supporter", icon: "7060786766c9c840eb3019e725d2b358", link: "https://discord.com/settings/premium" }, + "verified_developer": { id: "verified_developer", description: "Early Verified Bot Developer", icon: "6df5892e0f35b051f8b61eace34f4967" }, +}; + const fetching = new Set(); const queue = new Queue(5); +interface ProfileBadge { + id: string; + description: string; + icon: string; + link?: string; +} + interface MentionProps { data: { userId?: string; @@ -43,6 +68,45 @@ interface MentionProps { UserMention: ComponentType; } +async function getUser(id: string) { + let userObj = UserStore.getUser(id); + if (userObj) + return userObj; + + const user: any = await RestAPI.get({ url: `/users/${id}` }).then(response => { + FluxDispatcher.dispatch({ + type: "USER_UPDATE", + user: response.body, + }); + + return response.body; + }); + + // Populate the profile + await FluxDispatcher.dispatch( + { + type: "USER_PROFILE_FETCH_FAILURE", + userId: id, + } + ); + + userObj = UserStore.getUser(id); + const fakeBadges: ProfileBadge[] = Object.entries(UserFlags) + .filter(([_, flag]) => !isNaN(flag) && userObj.hasFlag(flag)) + .map(([key]) => badges[key.toLowerCase()]); + if (user.premium_type || !user.bot && (user.banner || user.avatar?.startsWith?.("a_"))) + fakeBadges.push(badges.premium); + + // Fill in what we can deduce + const profile = UserProfileStore.getUserProfile(id); + profile.accentColor = user.accent_color; + profile.badges = fakeBadges; + profile.banner = user.banner; + profile.premiumType = user.premium_type; + + return userObj; +} + function MentionWrapper({ data, UserMention, RoleMention, parse, props }: MentionProps) { const [userId, setUserId] = useState(data.userId); @@ -85,14 +149,14 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio fetching.add(id); queue.unshift(() => - UserUtils.getUser(id) + getUser(id) .then(() => { setUserId(id); fetching.delete(id); }) .catch(e => { if (e?.status === 429) { - queue.unshift(() => sleep(1000).then(fetch)); + queue.unshift(() => sleep(e?.body?.retry_after ?? 1000).then(fetch)); fetching.delete(id); } }) @@ -112,7 +176,7 @@ function MentionWrapper({ data, UserMention, RoleMention, parse, props }: Mentio export default definePlugin({ name: "ValidUser", description: "Fix mentions for unknown users showing up as '@unknown-user' (hover over a mention to fix it)", - authors: [Devs.Ven], + authors: [Devs.Ven, Devs.Dolfies], tags: ["MentionCacheFix"], patches: [ diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts index f5d30cd76..6d74e9b25 100644 --- a/src/webpack/common/utils.ts +++ b/src/webpack/common/utils.ts @@ -23,7 +23,6 @@ import { _resolveReady, filters, findByCodeLazy, findByProps, findByPropsLazy, f import type * as t from "./types/utils"; export let FluxDispatcher: t.FluxDispatcher; - waitFor(["dispatch", "subscribe"], m => { FluxDispatcher = m; // Non import call to avoid circular dependency @@ -40,6 +39,8 @@ export let ComponentDispatch; waitFor(["ComponentDispatch", "ComponentDispatcher"], m => ComponentDispatch = m.ComponentDispatch); +export const Constants = findByPropsLazy("Endpoints"); + export const RestAPI: t.RestAPI = proxyLazyWebpack(() => { const mod = findByProps("getAPIBaseURL"); return mod.HTTP ?? mod; From 04d7cb8797b3f15a97228c7b9f051cd7788aeb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ruiz=20P=C3=A9rez?= <57387907+GabiRP@users.noreply.github.com> Date: Sat, 4 May 2024 01:15:18 +0200 Subject: [PATCH 15/16] NewGuildSettings: add push notifs, highlights & events (#2413) Co-authored-by: V --- src/plugins/newGuildSettings/index.tsx | 34 +++++++++++++++++++++++--- src/utils/constants.ts | 4 +++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/plugins/newGuildSettings/index.tsx b/src/plugins/newGuildSettings/index.tsx index ff6f1c261..d5c3d9206 100644 --- a/src/plugins/newGuildSettings/index.tsx +++ b/src/plugins/newGuildSettings/index.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { definePluginSettings,migratePluginSettings } from "@api/Settings"; +import { definePluginSettings, migratePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { findByPropsLazy } from "@webpack"; @@ -31,6 +31,16 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true }, + messages: { + description: "Server Notification Settings", + type: OptionType.SELECT, + options: [ + { label: "All messages", value: 0 }, + { label: "Only @mentions", value: 1 }, + { label: "Nothing", value: 2 }, + { label: "Server default", value: 3, default: true } + ], + }, everyone: { description: "Suppress @everyone and @here", type: OptionType.BOOLEAN, @@ -41,6 +51,16 @@ const settings = definePluginSettings({ type: OptionType.BOOLEAN, default: true }, + highlights: { + description: "Suppress Highlights automatically", + type: OptionType.BOOLEAN, + default: true + }, + events: { + description: "Mute New Events automatically", + type: OptionType.BOOLEAN, + default: true + }, showAllChannels: { description: "Show all channels automatically", type: OptionType.BOOLEAN, @@ -53,7 +73,7 @@ export default definePlugin({ name: "NewGuildSettings", description: "Automatically mute new servers and change various other settings upon joining", tags: ["MuteNewGuild", "mute", "server"], - authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi], + authors: [Devs.Glitch, Devs.Nuckyz, Devs.carince, Devs.Mopi, Devs.GabiRP], patches: [ { find: ",acceptInvite(", @@ -78,8 +98,16 @@ export default definePlugin({ { muted: settings.store.guild, suppress_everyone: settings.store.everyone, - suppress_roles: settings.store.role + suppress_roles: settings.store.role, + mute_scheduled_events: settings.store.events, + notify_highlights: settings.store.highlights ? 1 : 0 }); + if (settings.store.messages !== 3) { + updateGuildNotificationSettings(guildId, + { + message_notifications: settings.store.messages, + }); + } if (settings.store.showAllChannels && isOptInEnabledForGuild(guildId)) { toggleShowAllChannels(guildId); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 8ab0bffb3..cce276ef8 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -449,6 +449,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ PolisanTheEasyNick: { name: "Oleh Polisan", id: 242305263313485825n + }, + GabiRP: { + name: "GabiRP", + id: 507955112027750401n } } satisfies Record); From dfb06e47d01e148646f682734b560ec4d7df34a8 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 4 May 2024 01:39:20 +0200 Subject: [PATCH 16/16] fix overflow when having two rows of badges --- src/plugins/_api/badges/fixBadgeOverflow.css | 3 +++ src/plugins/_api/{badges.tsx => badges/index.tsx} | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 src/plugins/_api/badges/fixBadgeOverflow.css rename src/plugins/_api/{badges.tsx => badges/index.tsx} (99%) diff --git a/src/plugins/_api/badges/fixBadgeOverflow.css b/src/plugins/_api/badges/fixBadgeOverflow.css new file mode 100644 index 000000000..348d0ff38 --- /dev/null +++ b/src/plugins/_api/badges/fixBadgeOverflow.css @@ -0,0 +1,3 @@ +[class*="profileBadges"] { + flex: none; +} diff --git a/src/plugins/_api/badges.tsx b/src/plugins/_api/badges/index.tsx similarity index 99% rename from src/plugins/_api/badges.tsx rename to src/plugins/_api/badges/index.tsx index 3cfbb4d99..5493d4b6a 100644 --- a/src/plugins/_api/badges.tsx +++ b/src/plugins/_api/badges/index.tsx @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import "./fixBadgeOverflow.css"; + import { BadgePosition, BadgeUserArgs, ProfileBadge } from "@api/Badges"; import DonateButton from "@components/DonateButton"; import ErrorBoundary from "@components/ErrorBoundary";