From 98cb301df53305f397ac6e1b4e603c930820f228 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 31 Aug 2022 04:07:16 +0200 Subject: [PATCH] Make Settings & Settings Page --- build.mjs | 2 +- src/Vencord.ts | 2 +- src/VencordNative.ts | 37 +++++++--- src/api/settings.ts | 81 +++++++++++++++++++++ src/components/Settings.tsx | 84 +++++++++++++++++++++- src/ipcMain.ts | 28 ++++++-- src/plugins/index.ts | 5 +- src/plugins/messageActions.ts | 3 +- src/plugins/noTrack.ts | 3 +- src/plugins/settings.ts | 3 +- src/utils/IpcEvents.ts | 22 ++++++ src/utils/ipcEvents.ts | 3 - src/utils/misc.tsx | 61 ++++++++++++++++ src/utils/patchWebpack.ts | 12 ++-- src/utils/quickCss.ts | 6 +- src/utils/types.ts | 2 + src/{utils/webpack.ts => webpack/index.ts} | 17 +++-- tsconfig.json | 2 +- 18 files changed, 330 insertions(+), 43 deletions(-) create mode 100644 src/api/settings.ts create mode 100644 src/utils/IpcEvents.ts delete mode 100644 src/utils/ipcEvents.ts create mode 100644 src/utils/misc.tsx rename src/{utils/webpack.ts => webpack/index.ts} (91%) diff --git a/build.mjs b/build.mjs index f2a44474..c3bac3ba 100755 --- a/build.mjs +++ b/build.mjs @@ -117,7 +117,7 @@ await Promise.all([ ], sourcemap: "inline", watch, - minify: true + minify: false, }) ]).then(res => { const took = performance.now() - begin; diff --git a/src/Vencord.ts b/src/Vencord.ts index 43644683..8ef272be 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -1,5 +1,5 @@ export * as Plugins from "./plugins"; -export * as Webpack from "./utils/webpack"; +export * as Webpack from "./webpack"; export * as Api from "./api"; export * as Components from "./components"; diff --git a/src/VencordNative.ts b/src/VencordNative.ts index 7fbe6dfa..fc1e690d 100644 --- a/src/VencordNative.ts +++ b/src/VencordNative.ts @@ -1,12 +1,33 @@ -import { IPC_QUICK_CSS_UPDATE, IPC_GET_QUICK_CSS } from './utils/ipcEvents'; -import { ipcRenderer } from 'electron'; +import IPC_EVENTS from './utils/IpcEvents'; +import { IpcRenderer, ipcRenderer } from 'electron'; export default { - handleQuickCssUpdate(cb: (s: string) => void) { - ipcRenderer.on(IPC_QUICK_CSS_UPDATE, (_, css) => { - cb(css); - }); + getVersions: () => process.versions, + ipc: { + send(event: string, ...args: any[]) { + if (event in IPC_EVENTS) ipcRenderer.send(event, ...args); + else throw new Error(`Event ${event} not allowed.`); + }, + sendSync(event: string, ...args: any[]) { + if (event in IPC_EVENTS) return ipcRenderer.sendSync(event, ...args); + else throw new Error(`Event ${event} not allowed.`); + }, + on(event: string, listener: Parameters[1]) { + if (event in IPC_EVENTS) ipcRenderer.on(event, listener); + else throw new Error(`Event ${event} not allowed.`); + }, + invoke(event: string, ...args: any[]) { + if (event in IPC_EVENTS) return ipcRenderer.invoke(event, ...args); + else throw new Error(`Event ${event} not allowed.`); + } }, - getQuickCss: () => ipcRenderer.invoke(IPC_GET_QUICK_CSS) as Promise, - getVersions: () => process.versions + require(mod: string) { + const settings = ipcRenderer.sendSync(IPC_EVENTS.GET_SETTINGS); + try { + if (!JSON.parse(settings).unsafeRequire) throw "no"; + } catch { + throw new Error("Unsafe require is not allowed. Enable it in settings and try again."); + } + return require(mod); + } }; \ No newline at end of file diff --git a/src/api/settings.ts b/src/api/settings.ts new file mode 100644 index 00000000..253c7263 --- /dev/null +++ b/src/api/settings.ts @@ -0,0 +1,81 @@ +import plugins from "plugins"; +import IpcEvents from "../utils/IpcEvents"; +import { React } from "../webpack"; +import { mergeDefaults } from '../utils/misc'; + +interface Settings { + unsafeRequire: boolean; + plugins: { + [plugin: string]: { + enabled: boolean; + [setting: string]: any; + }; + }; +} + +const DefaultSettings: Settings = { + unsafeRequire: false, + plugins: {} +}; + +for (const plugin of plugins) { + DefaultSettings.plugins[plugin.name] = { + enabled: plugin.required ?? false + }; +} + +try { + var settings = JSON.parse(VencordNative.ipc.sendSync(IpcEvents.GET_SETTINGS)) as Settings; + for (const key in DefaultSettings) { + settings[key] ??= DefaultSettings[key]; + } + mergeDefaults(settings, DefaultSettings); +} catch (err) { + console.error("Corrupt settings file. ", err); + var settings = mergeDefaults({} as Settings, DefaultSettings); +} + +const subscriptions = new Set<() => void>(); + +function makeProxy(settings: Settings, root = settings): Settings { + return new Proxy(settings, { + get(target, p) { + const v = target[p]; + if (typeof v === "object" && !Array.isArray(v)) return makeProxy(v, root); + return v; + }, + set(target, p, v) { + if (target[p] === v) return true; + + target[p] = v; + for (const subscription of subscriptions) { + subscription(); + } + VencordNative.ipc.invoke(IpcEvents.SET_SETTINGS, JSON.stringify(root)); + return true; + } + }); +} + +/** + * A smart settings object. Altering props automagically saves + * the updated settings to disk. + */ +export const Settings = makeProxy(settings); + +/** + * Settings hook for React components. Returns a smart settings + * object that automagically triggers a rerender if any properties + * are altered + * @returns Settings + */ +export function useSettings() { + const [, forceUpdate] = React.useReducer(x => ({}), {}); + + React.useEffect(() => { + subscriptions.add(forceUpdate); + return () => void subscriptions.delete(forceUpdate); + }, []); + + return Settings; +} \ No newline at end of file diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index f9964486..4499c9ff 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -1,4 +1,84 @@ +import { lazy, LazyComponent, useAwaiter } from "../utils/misc"; +import { findByDisplayName, Forms } from '../webpack'; +import Plugins from 'plugins'; +import { useSettings } from "../api/settings"; +import { findByProps } from '../webpack/index'; +import IpcEvents from "../utils/IpcEvents"; + +// Lazy spam because this is ran before React is a thing. Todo: Fix that and clean this up lmao + +const SwitchItem = LazyComponent void; + note?: string; + tooltipNote?: string; + disabled?: boolean; +}>>(() => findByDisplayName("SwitchItem").default); + +const getButton = lazy(() => findByProps("ButtonLooks", "default")); +const Button = LazyComponent(() => getButton().default); +const getFlex = lazy(() => findByDisplayName("Flex")); +const Flex = LazyComponent(() => getFlex().default); +const FlexChild = LazyComponent(() => getFlex().default.Child); +const getMargins = lazy(() => findByProps("marginTop8", "marginBottom8")); + export default function Settings(props) { - console.log(props); - return (

Hi

); + const settingsDir = useAwaiter(() => VencordNative.ipc.invoke(IpcEvents.GET_SETTINGS_DIR), "Loading..."); + const settings = useSettings(); + + return ( + + SettingsDir: {settingsDir} + + + + + + + + + Settings + settings.unsafeRequire = v} + note="Enables VencordNative.require. Useful for testing, very bad for security. Leave this off unless you need it." + > + Enable Ensafe Require + + + Plugins + {Plugins.map(p => ( + { + settings.plugins[p.name].enabled = v; + if (v) { + p.dependencies?.forEach(d => { + settings.plugins[d].enabled = true; + }); + } + }} + note={p.description} + tooltipNote={p.required ? "This plugin is required. Thus you cannot disable it." : undefined} + > + {p.name} + + )) + } + + ); } \ No newline at end of file diff --git a/src/ipcMain.ts b/src/ipcMain.ts index c8fba371..38a16ab5 100644 --- a/src/ipcMain.ts +++ b/src/ipcMain.ts @@ -1,25 +1,39 @@ -import { app, BrowserWindow, ipcMain } from "electron"; -import { fstat, watch } from "fs"; -import { open, readFile } from "fs/promises"; +import { app, BrowserWindow, ipcMain, shell } from "electron"; +import { readFileSync, watch } from "fs"; +import { open, readFile, writeFile } from "fs/promises"; import { join } from 'path'; -import { IPC_GET_SETTINGS_DIR, IPC_GET_QUICK_CSS, IPC_QUICK_CSS_UPDATE } from './utils/ipcEvents'; +import IpcEvents from './utils/IpcEvents'; const DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); const SETTINGS_DIR = join(DATA_DIR, "settings"); const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); +const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); function readCss() { return readFile(QUICKCSS_PATH, "utf-8").catch(() => ""); } -ipcMain.handle(IPC_GET_SETTINGS_DIR, () => SETTINGS_DIR); -ipcMain.handle(IPC_GET_QUICK_CSS, () => readCss()); +function readSettings() { + try { + return readFileSync(SETTINGS_FILE, "utf-8"); + } catch { + return "{}"; + } +} + +ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); +ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); +// .on because we need Settings synchronously (ipcRenderer.sendSync) +ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings()); +ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => void writeFile(SETTINGS_FILE, s)); +ipcMain.handle(IpcEvents.OPEN_PATH, (_, path) => shell.openPath(path)); +ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url)); export function initIpc(mainWindow: BrowserWindow) { open(QUICKCSS_PATH, "a+").then(fd => { fd.close(); watch(QUICKCSS_PATH, async () => { - mainWindow.webContents.postMessage(IPC_QUICK_CSS_UPDATE, await readCss()); + mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); }); }); } diff --git a/src/plugins/index.ts b/src/plugins/index.ts index d5b419be..ae67f4f0 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,4 +1,5 @@ import Plugins from "plugins"; +import { Settings } from "../api/settings"; import Logger from "../utils/logger"; import { Patch } from "../utils/types"; @@ -7,7 +8,7 @@ const logger = new Logger("PluginManager", "#a6d189"); export const plugins = Plugins; export const patches = [] as Patch[]; -for (const plugin of Plugins) if (plugin.patches) { +for (const plugin of Plugins) if (plugin.patches && Settings.plugins[plugin.name].enabled) { for (const patch of plugin.patches) { patch.plugin = plugin.name; if (!Array.isArray(patch.replacement)) patch.replacement = [patch.replacement]; @@ -16,7 +17,7 @@ for (const plugin of Plugins) if (plugin.patches) { } export function startAll() { - for (const plugin of plugins) if (plugin.start) { + for (const plugin of plugins) if (plugin.start && Settings.plugins[plugin.name].enabled) { try { logger.info("Starting plugin", plugin.name); plugin.start(); diff --git a/src/plugins/messageActions.ts b/src/plugins/messageActions.ts index c2857cbd..5519cf73 100644 --- a/src/plugins/messageActions.ts +++ b/src/plugins/messageActions.ts @@ -1,11 +1,12 @@ import { MessageClicks } from "../api"; import definePlugin from "../utils/types"; -import { find, findByProps } from "../utils/webpack"; +import { find, findByProps } from "../webpack"; export default definePlugin({ name: "MessageQuickActions", description: "Quick Delete, Quick edit", author: "Vendicated", + dependencies: ["MessageClicksApi"], start() { const { deleteMessage, startEditMessage } = findByProps("deleteMessage"); const { can } = findByProps("can", "initialize"); diff --git a/src/plugins/noTrack.ts b/src/plugins/noTrack.ts index 6d3a0ba9..642d88c7 100644 --- a/src/plugins/noTrack.ts +++ b/src/plugins/noTrack.ts @@ -1,5 +1,5 @@ import definePlugin from "../utils/types"; -import { findByProps } from "../utils/webpack"; +import { findByProps } from "../webpack"; const DO_NOTHING = () => void 0; @@ -7,6 +7,7 @@ export default definePlugin({ name: "NoTrack", description: "Disable Discord's tracking and crash reporting", author: "Vendicated", + required: true, start() { findByProps("getSuperPropertiesBase64", "track").track = DO_NOTHING; findByProps("submitLiveCrashReport").submitLiveCrashReport = DO_NOTHING; diff --git a/src/plugins/settings.ts b/src/plugins/settings.ts index fa214f09..63c36d6b 100644 --- a/src/plugins/settings.ts +++ b/src/plugins/settings.ts @@ -5,6 +5,7 @@ export default definePlugin({ name: "Settings", description: "Adds Settings UI and debug info", author: "Vendicated", + required: true, patches: [{ find: "default.versionHash", replacement: [ @@ -28,7 +29,7 @@ export default definePlugin({ }, { find: "Messages.ACTIVITY_SETTINGS", replacement: { - match: /\{section:(.{1,2})\.SectionTypes\.HEADER,label:(.{1,2})\.default\.Messages\.ACTIVITY_SETTINGS\}/, + match: /\{section:(.{1,2})\.SectionTypes\.HEADER,\s*label:(.{1,2})\.default\.Messages\.ACTIVITY_SETTINGS\}/, replace: (m, mod) => `{section:${mod}.SectionTypes.HEADER,label:"Vencord"},` + `{section:"Vencord",label:"Vencord",element:Vencord.Components.Settings},` + diff --git a/src/utils/IpcEvents.ts b/src/utils/IpcEvents.ts new file mode 100644 index 00000000..70ba5020 --- /dev/null +++ b/src/utils/IpcEvents.ts @@ -0,0 +1,22 @@ +type Enum> = { + [k in keyof T]: T[k]; +} & { [v in keyof T as T[v]]: v; }; + +function strEnum>(obj: T): T { + const o = {} as T; + for (const key in obj) { + o[key] = obj[key] as any; + o[obj[key]] = key as any; + }; + return o; +} + +export default strEnum({ + QUICK_CSS_UPDATE: "VencordQuickCssUpdate", + GET_QUICK_CSS: "VencordGetQuickCss", + GET_SETTINGS_DIR: "VencordGetSettingsDir", + GET_SETTINGS: "VencordGetSettings", + SET_SETTINGS: "VencordSetSettings", + OPEN_EXTERNAL: "VencordOpenExternal", + OPEN_PATH: "VencordOpenPath", +} as const); \ No newline at end of file diff --git a/src/utils/ipcEvents.ts b/src/utils/ipcEvents.ts deleted file mode 100644 index 1920023a..00000000 --- a/src/utils/ipcEvents.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const IPC_QUICK_CSS_UPDATE = "VencordQuickCssUpdate"; -export const IPC_GET_QUICK_CSS = "VencordGetQuickCss"; -export const IPC_GET_SETTINGS_DIR = "VencordGetSettingsDir"; \ No newline at end of file diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx new file mode 100644 index 00000000..ded052b3 --- /dev/null +++ b/src/utils/misc.tsx @@ -0,0 +1,61 @@ +import { React } from "../webpack"; + +/** + * Makes a lazy function. On first call, the value is computed. + * On subsequent calls, the same computed value will be returned + * @param factory Factory function + */ +export function lazy(factory: () => T): () => T { + let cache: T; + return () => { + return cache ?? (cache = factory()); + }; +} + +/** + * Await a promise + * @param factory Factory + * @param fallbackValue The fallback value that will be used until the promise resolved + * @returns A state that will either be null or the result of the promise + */ +export function useAwaiter(factory: () => Promise, fallbackValue: T | null = null): T | null { + const [res, setRes] = React.useState(fallbackValue); + + React.useEffect(() => { + factory().then(setRes); + }, []); + + return res; +} + +/** + * A lazy component. The factory method is called on first render. For example useful + * for const Component = LazyComponent(() => findByDisplayName("...").default) + * @param factory Function returning a Component + * @returns Result of factory function + */ +export function LazyComponent(factory: () => React.ComponentType) { + return (props: T) => { + const Component = React.useMemo(factory, []); + return ; + }; +} + +/** + * Recursively merges defaults into an object and returns the same object + * @param obj Object + * @param defaults Defaults + * @returns obj + */ +export function mergeDefaults(obj: T, defaults: T): T { + for (const key in defaults) { + const v = defaults[key]; + if (typeof v === "object" && !Array.isArray(v)) { + obj[key] ??= {} as any; + mergeDefaults(obj[key], v); + } else { + obj[key] ??= v; + } + } + return obj; +} \ No newline at end of file diff --git a/src/utils/patchWebpack.ts b/src/utils/patchWebpack.ts index 0e946944..9f4b4350 100644 --- a/src/utils/patchWebpack.ts +++ b/src/utils/patchWebpack.ts @@ -1,6 +1,6 @@ import { WEBPACK_CHUNK } from './constants'; import Logger from "./logger"; -import { _initWebpack } from "./webpack"; +import { _initWebpack } from "../webpack"; let webpackChunk: any[]; @@ -83,9 +83,13 @@ function patchPush() { const lastCode = code; try { const newCode = code.replace(replacement.match, replacement.replace); - const newMod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); - code = newCode; - mod = newMod; + if (newCode === code) { + logger.warn(`Patch by ${patch.plugin} had no effect: ${replacement.match}`); + } else { + const newMod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); + code = newCode; + mod = newMod; + } } catch (err) { logger.error("Failed to apply patch of", patch.plugin, err); code = lastCode; diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index 724fde87..5c9e8301 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -1,6 +1,8 @@ +import IpcEvents from "./IpcEvents"; + document.addEventListener("DOMContentLoaded", async () => { const style = document.createElement("style"); document.head.appendChild(style); - VencordNative.handleQuickCssUpdate((css: string) => style.innerText = css); - style.innerText = await VencordNative.getQuickCss(); + VencordNative.ipc.on(IpcEvents.QUICK_CSS_UPDATE, (_, css: string) => style.innerText = css); + style.innerText = await VencordNative.ipc.invoke(IpcEvents.GET_QUICK_CSS); }); diff --git a/src/utils/types.ts b/src/utils/types.ts index 282ca0e4..520b5064 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -20,6 +20,8 @@ export interface Plugin { author: string; start?(): void; patches?: Patch[]; + dependencies?: string[], + required?: boolean; } // @ts-ignore lole diff --git a/src/utils/webpack.ts b/src/webpack/index.ts similarity index 91% rename from src/utils/webpack.ts rename to src/webpack/index.ts index 3f21106a..2c5a4557 100644 --- a/src/utils/webpack.ts +++ b/src/webpack/index.ts @@ -1,5 +1,4 @@ import { startAll } from "../plugins"; -import Logger from "./logger"; let webpackCache: typeof window.webpackChunkdiscord_app; @@ -9,11 +8,10 @@ export const listeners = new Set(); type FilterFn = (mod: any) => boolean; type CallbackFn = (mod: any) => void; -export let Common: { - React: typeof import("react"), - FluxDispatcher: any; - UserStore: any; -} = {} as any; +export let React: typeof import("react"); +export let FluxDispatcher: any; +export let Forms: any; +export let UserStore: any; export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { if (webpackCache !== void 0) throw "no."; @@ -24,9 +22,9 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { // Abandon Hope All Ye Who Enter Here let started = false; - waitFor("getCurrentUser", x => Common.UserStore = x); + waitFor("getCurrentUser", x => UserStore = x); waitFor(["dispatch", "subscribe"], x => { - Common.FluxDispatcher = x; + FluxDispatcher = x; const cb = () => { console.info("Connection open"); x.unsubscribe("CONNECTION_OPEN", cb); @@ -34,7 +32,8 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { }; x.subscribe("CONNECTION_OPEN", cb); }); - waitFor("useState", x => Common.React = x); + waitFor("useState", x => (React = x)); + waitFor("FormSection", x => Forms = x); } export function find(filter: FilterFn, getDefault = true) { diff --git a/tsconfig.json b/tsconfig.json index 6489d934..d17ff098 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "noImplicitAny": false, "target": "ESNEXT", // https://esbuild.github.io/api/#jsx-factory - "jsxFactory": "Vencord.Webpack.Common.React.createElement", + "jsxFactory": "Vencord.Webpack.React.createElement", "jsx": "react" }, "include": ["src/**/*"]