From 0ba9ee600cc31f99024549693abcdb16e0c844e8 Mon Sep 17 00:00:00 2001 From: Beef Date: Sat, 4 Feb 2023 02:30:46 +0000 Subject: [PATCH] [Global] Major refactors, allow unloading! --- src/index.ts | 94 +++++-------------- src/lib/commands.ts | 9 +- src/lib/debug.ts | 14 +-- src/lib/metro/filters.ts | 4 +- src/lib/plugins.ts | 20 ++-- src/lib/windowObject.ts | 54 +++++++++++ src/ui/assets.ts | 26 ++--- src/ui/fixTheme.ts | 2 +- .../settings/components/SettingsSection.tsx | 8 +- src/ui/settings/index.tsx | 22 ++--- 10 files changed, 137 insertions(+), 116 deletions(-) create mode 100644 src/lib/windowObject.ts diff --git a/src/index.ts b/src/index.ts index b2376ce..48d1242 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,79 +1,37 @@ -import patcher from "@lib/patcher"; -import logger from "@lib/logger"; -import copyText from "@utils/copyText"; -import findInReactTree from "@utils/findInReactTree"; -import findInTree from "@utils/findInTree"; -import * as constants from "@lib/constants"; -import * as metro from "@metro/filters"; -import * as common from "@metro/common"; -import * as components from "@ui/components"; -import * as toasts from "@ui/toasts"; -import * as storage from "@lib/storage"; -import { patchAssets, all, find, getAssetByID, getAssetByName, getAssetIDByName } from "@ui/assets"; +import { patchLogHook } from "@lib/debug"; +import { patchCommands } from "@lib/commands"; +import { initPlugins } from "@lib/plugins"; +import { patchAssets } from "@ui/assets"; import initSettings from "@ui/settings"; -import { fixTheme } from "@ui/fixTheme"; -import { connectToDebugger, patchLogHook, versionHash } from "@lib/debug"; -import { plugins, fetchPlugin, evalPlugin, stopPlugin, removePlugin, getSettings, initializePlugins } from "@lib/plugins"; -import settings from "@lib/settings"; -import { registerCommand } from "@lib/commands"; +import fixTheme from "@ui/fixTheme"; +import windowObject from "@lib/windowObject"; +import logger from "@lib/logger"; +// This logs in the native logging implementation, e.g. logcat console.log("Hello from Vendetta!"); async function init() { - let erroredOnLoad = false; - try { - window.vendetta = { - patcher: patcher, - metro: { ...metro, common: { ...common } }, - constants: { ...constants }, - utils: { - copyText: copyText, - findInReactTree: findInReactTree, - findInTree: findInTree, - }, - debug: { - connectToDebugger: connectToDebugger, - }, - ui: { - components: { ...components }, - toasts: { ...toasts }, - assets: { - all: all, - find: find, - getAssetByID: getAssetByID, - getAssetByName: getAssetByName, - getAssetIDByName: getAssetIDByName, - }, - }, - plugins: { - plugins: plugins, - fetchPlugin: fetchPlugin, - evalPlugin: evalPlugin, - stopPlugin: stopPlugin, - removePlugin: removePlugin, - getSettings: getSettings, - }, - commands: { - registerCommand: registerCommand, - }, - storage: { ...storage }, - settings: settings, - logger: logger, - version: versionHash, - }; + // Load everything in parallel + const unloads = await Promise.all([ + patchLogHook(), + patchAssets(), + patchCommands(), + fixTheme(), + initSettings(), + ]); - patchLogHook(); - patchAssets(); - fixTheme(); - initializePlugins(); - initSettings(); - } catch (e: Error | any) { - erroredOnLoad = true; + // Assign window object + window.vendetta = await windowObject(unloads); + + // Once done, load plugins + unloads.push(await initPlugins()); + + // We good :) + logger.log("Vendetta is ready!"); + } catch (e: any) { alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`); } - - if (!erroredOnLoad) logger.log("Vendetta is ready!"); }; -init(); +init(); \ No newline at end of file diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 9a12346..b6f77bb 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -3,10 +3,15 @@ import { findByProps } from "@metro/filters"; import { after } from "@lib/patcher"; const commandsModule = findByProps("getBuiltInCommands") - let commands: ApplicationCommand[] = []; -after("getBuiltInCommands", commandsModule, (args, res) => res.concat(commands)); +export function patchCommands() { + const unpatch = after("getBuiltInCommands", commandsModule, (args, res) => res.concat(commands)); + return () => { + commands = []; + unpatch(); + } +} export function registerCommand(command: ApplicationCommand): () => void { // Get built in commands diff --git a/src/lib/debug.ts b/src/lib/debug.ts index 7d83772..7711a35 100644 --- a/src/lib/debug.ts +++ b/src/lib/debug.ts @@ -34,14 +34,16 @@ export function connectToDebugger(url: string) { }); } -export function patchLogHook() { - after("nativeLoggingHook", globalThis, (args, ret) => { - if (socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ message: args[0], level: args[1] })); - } - +export function patchLogHook() { + const unpatch = after("nativeLoggingHook", globalThis, (args) => { + if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] })); logger.log(args[0]); }); + + return () => { + socket && socket.close(); + unpatch(); + } } export const versionHash = "__vendettaVersion"; diff --git a/src/lib/metro/filters.ts b/src/lib/metro/filters.ts index d41aca3..6ef5dd7 100644 --- a/src/lib/metro/filters.ts +++ b/src/lib/metro/filters.ts @@ -26,7 +26,7 @@ for (const key in window.modules) { } // Function to filter through modules -export const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => { +const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => { const found = []; // Get the previous moment locale @@ -53,7 +53,7 @@ export const filterModules = (modules: MetroModules, single = false) => (filter: found.push(module.default); } - if(filter(module)) { + if (filter(module)) { if (single) return module; else found.push(module); } diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts index 17e351e..99bb7cb 100644 --- a/src/lib/plugins.ts +++ b/src/lib/plugins.ts @@ -4,6 +4,8 @@ import { awaitSyncWrapper, createStorage, wrapSync } from "@lib/storage"; import logger from "@lib/logger"; import Subpage from "@ui/settings/components/Subpage"; +// TODO: Properly implement hash-based updating + type EvaledPlugin = { onLoad?(): void; onUnload(): void; @@ -20,7 +22,7 @@ export async function fetchPlugin(id: string, enabled = true) { let pluginManifest: PluginManifest; try { - pluginManifest = await (await fetch(new URL("manifest.json", id), { cache: "no-store" })).json(); + pluginManifest = await (await fetch(id + "manifest.json", { cache: "no-store" })).json(); } catch { throw new Error(`Failed to fetch manifest for ${id}`); } @@ -30,7 +32,7 @@ export async function fetchPlugin(id: string, enabled = true) { // TODO: Remove duplicate error if possible try { // by polymanifest spec, plugins should always specify their main file, but just in case - pluginJs = await (await fetch(new URL(pluginManifest.main || "index.js", id), { cache: "no-store" })).text(); + pluginJs = await (await fetch(id + (pluginManifest.main || "index.js"), { cache: "no-store" })).text(); } catch { throw new Error(`Failed to fetch JS for ${id}`); } @@ -49,7 +51,6 @@ export async function fetchPlugin(id: string, enabled = true) { } export async function evalPlugin(plugin: Plugin) { - // TODO: Refactor to not depend on own window object const vendettaForPlugins = { ...window.vendetta, plugin: { @@ -89,7 +90,7 @@ export async function startPlugin(id: string) { } } -export function stopPlugin(id: string) { +export function stopPlugin(id: string, disable = true) { const plugin = plugins[id]; const pluginRet = loadedPlugins[id]; if (!plugin) throw new Error("Attempted to stop non-existent plugin"); @@ -102,7 +103,7 @@ export function stopPlugin(id: string) { } delete loadedPlugins[id]; - plugin.enabled = false; + disable && (plugin.enabled = false); } export function removePlugin(id: string) { @@ -111,15 +112,18 @@ export function removePlugin(id: string) { delete plugins[id]; } -export async function initializePlugins() { +export async function initPlugins() { await awaitSyncWrapper(plugins); const allIds = Object.keys(plugins); await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false))); - for (const pl of allIds.filter((pl) => plugins[pl].enabled)) - startPlugin(pl); + for (const pl of allIds.filter((pl) => plugins[pl].enabled)) startPlugin(pl); + + return stopAllPlugins; } +const stopAllPlugins = () => Object.keys(plugins).forEach(p => stopPlugin(p, false)); + export const getSettings = (id: string) => loadedPlugins[id]?.settings; export function showSettings(plugin: Plugin) { diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts new file mode 100644 index 0000000..197664b --- /dev/null +++ b/src/lib/windowObject.ts @@ -0,0 +1,54 @@ +import { VendettaObject } from "@types"; +import patcher from "@lib/patcher"; +import logger from "@lib/logger"; +import settings from "@lib/settings"; +import copyText from "@utils/copyText"; +import findInReactTree from "@utils/findInReactTree"; +import findInTree from "@utils/findInTree"; +import * as constants from "@lib/constants"; +import * as debug from "@lib/debug"; +import * as plugins from "@lib/plugins"; +import * as commands from "@lib/commands"; +import * as storage from "@lib/storage"; +import * as metro from "@metro/filters"; +import * as common from "@metro/common"; +import * as components from "@ui/components"; +import * as toasts from "@ui/toasts"; +import * as assets from "@ui/assets"; + +function without>(object: T, ...keys: string[]) { + const cloned = { ...object }; + keys.forEach((k) => delete cloned[k]); + return cloned; +} + +// I wish Hermes let me do async arrow functions +export default async function windowObject(unloads: any[]): Promise { + return { + patcher: without(patcher, "unpatchAll"), + metro: { ...metro, common: { ...common } }, + constants: { ...constants }, + utils: { + copyText: copyText, + findInReactTree: findInReactTree, + findInTree: findInTree, + }, + debug: without(debug, "versionHash", "patchLogHook"), + ui: { + components, + toasts, + assets, + }, + plugins: without(plugins, "initPlugins"), + commands: without(commands, "patchCommands"), + storage, + settings, + logger, + version: debug.versionHash, + unload: () => { + unloads.filter(i => typeof i === "function").forEach(p => p()); + // @ts-expect-error explode + delete window.vendetta; + } + } +} \ No newline at end of file diff --git a/src/ui/assets.ts b/src/ui/assets.ts index e8f6eec..1c31ff8 100644 --- a/src/ui/assets.ts +++ b/src/ui/assets.ts @@ -1,23 +1,23 @@ -import { Asset, Indexable, } from "@types"; +import { Asset, Indexable } from "@types"; import { after } from "@lib/patcher"; import { assets } from "@metro/common"; export const all: Indexable = {}; export function patchAssets() { - try { - after("registerAsset", assets, (args: Asset[], id: number) => { - const asset = args[0]; - all[asset.name] = { ...asset, id: id }; - }); + const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => { + const asset = args[0]; + all[asset.name] = { ...asset, id: id }; + }); - for (let id = 1; ; id++) { - const asset = assets.getAssetByID(id); - if (!asset) break; - if (all[asset.name]) continue; - all[asset.name] = { ...asset, id: id }; - }; - } catch {}; + for (let id = 1; ; id++) { + const asset = assets.getAssetByID(id); + if (!asset) break; + if (all[asset.name]) continue; + all[asset.name] = { ...asset, id: id }; + }; + + return unpatch; } export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter); diff --git a/src/ui/fixTheme.ts b/src/ui/fixTheme.ts index 058db09..20daf87 100644 --- a/src/ui/fixTheme.ts +++ b/src/ui/fixTheme.ts @@ -18,7 +18,7 @@ function override() { FluxDispatcher.unsubscribe("I18N_LOAD_START", override); } -export function fixTheme() { +export default function fixTheme() { try { if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override); } catch(e) { diff --git a/src/ui/settings/components/SettingsSection.tsx b/src/ui/settings/components/SettingsSection.tsx index b2045ca..5e3ac30 100644 --- a/src/ui/settings/components/SettingsSection.tsx +++ b/src/ui/settings/components/SettingsSection.tsx @@ -1,3 +1,4 @@ +import { NavigationNative } from "@metro/common"; import { Forms } from "@ui/components"; import { getAssetIDByName } from "@ui/assets"; import { useProxy } from "@lib/storage"; @@ -5,11 +6,8 @@ import settings from "@lib/settings"; const { FormRow, FormSection, FormDivider } = Forms; -interface SettingsSectionProps { - navigation: any; -} - -export default function SettingsSection({ navigation }: SettingsSectionProps) { +export default function SettingsSection() { + const navigation = NavigationNative.useNavigation(); useProxy(settings); return ( diff --git a/src/ui/settings/index.tsx b/src/ui/settings/index.tsx index fe12602..2c63dc4 100644 --- a/src/ui/settings/index.tsx +++ b/src/ui/settings/index.tsx @@ -9,10 +9,11 @@ import Developer from "@ui/settings/pages/Developer"; const screensModule = findByDisplayName("getScreens", false); const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false); -let prevPatches: Function[] = []; export default function initSettings() { - after("default", screensModule, (args, existingScreens) => { + const patches = new Array; + + patches.push(after("default", screensModule, (args, existingScreens) => { return { ...existingScreens, VendettaSettings: { @@ -28,24 +29,23 @@ export default function initSettings() { render: Developer } } - }); - - after("default", settingsModule, (args, ret) => { - for (let p of prevPatches) p(); - prevPatches = []; + })); + after("default", settingsModule, (_, ret) => { const Overview = findInReactTree(ret.props.children, i => i.type && i.type.name === "UserSettingsOverview"); // Upload logs button gone - prevPatches.push(after("renderSupportAndAcknowledgements", Overview.type.prototype, (args, { props: { children } }) => { + patches.push(after("renderSupportAndAcknowledgements", Overview.type.prototype, (_, { props: { children } }) => { const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton"); if (index !== -1) children.splice(index, 1); })); - prevPatches.push(after("render", Overview.type.prototype, (args, { props: { children } }) => { + patches.push(after("render", Overview.type.prototype, (_, { props: { children } }) => { const titles = [i18n.Messages["BILLING_SETTINGS"], i18n.Messages["PREMIUM_SETTINGS"]]; const index = children.findIndex((c: any) => titles.includes(c.props.title)); - children.splice(index === -1 ? 4 : index, 0, ); + children.splice(index === -1 ? 4 : index, 0, ); })); - }); + }, true); + + return () => patches.forEach(p => p()); } \ No newline at end of file