[Global] Major refactors, allow unloading!

This commit is contained in:
Beef 2023-02-04 02:30:46 +00:00
parent 0be8cdac05
commit 0ba9ee600c
10 changed files with 137 additions and 116 deletions

View file

@ -1,79 +1,37 @@
import patcher from "@lib/patcher"; import { patchLogHook } from "@lib/debug";
import logger from "@lib/logger"; import { patchCommands } from "@lib/commands";
import copyText from "@utils/copyText"; import { initPlugins } from "@lib/plugins";
import findInReactTree from "@utils/findInReactTree"; import { patchAssets } from "@ui/assets";
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 initSettings from "@ui/settings"; import initSettings from "@ui/settings";
import { fixTheme } from "@ui/fixTheme"; import fixTheme from "@ui/fixTheme";
import { connectToDebugger, patchLogHook, versionHash } from "@lib/debug"; import windowObject from "@lib/windowObject";
import { plugins, fetchPlugin, evalPlugin, stopPlugin, removePlugin, getSettings, initializePlugins } from "@lib/plugins"; import logger from "@lib/logger";
import settings from "@lib/settings";
import { registerCommand } from "@lib/commands";
// This logs in the native logging implementation, e.g. logcat
console.log("Hello from Vendetta!"); console.log("Hello from Vendetta!");
async function init() { async function init() {
let erroredOnLoad = false;
try { try {
window.vendetta = { // Load everything in parallel
patcher: patcher, const unloads = await Promise.all([
metro: { ...metro, common: { ...common } }, patchLogHook(),
constants: { ...constants }, patchAssets(),
utils: { patchCommands(),
copyText: copyText, fixTheme(),
findInReactTree: findInReactTree, initSettings(),
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,
};
patchLogHook(); // Assign window object
patchAssets(); window.vendetta = await windowObject(unloads);
fixTheme();
initializePlugins(); // Once done, load plugins
initSettings(); unloads.push(await initPlugins());
} catch (e: Error | any) {
erroredOnLoad = true; // We good :)
logger.log("Vendetta is ready!");
} catch (e: any) {
alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`); alert(`Vendetta failed to initialize... ${e.stack || e.toString()}`);
} }
if (!erroredOnLoad) logger.log("Vendetta is ready!");
}; };
init(); init();

View file

@ -3,10 +3,15 @@ import { findByProps } from "@metro/filters";
import { after } from "@lib/patcher"; import { after } from "@lib/patcher";
const commandsModule = findByProps("getBuiltInCommands") const commandsModule = findByProps("getBuiltInCommands")
let commands: ApplicationCommand[] = []; 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 { export function registerCommand(command: ApplicationCommand): () => void {
// Get built in commands // Get built in commands

View file

@ -34,14 +34,16 @@ export function connectToDebugger(url: string) {
}); });
} }
export function patchLogHook() { export function patchLogHook() {
after("nativeLoggingHook", globalThis, (args, ret) => { const unpatch = after("nativeLoggingHook", globalThis, (args) => {
if (socket?.readyState === WebSocket.OPEN) { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] }));
socket.send(JSON.stringify({ message: args[0], level: args[1] }));
}
logger.log(args[0]); logger.log(args[0]);
}); });
return () => {
socket && socket.close();
unpatch();
}
} }
export const versionHash = "__vendettaVersion"; export const versionHash = "__vendettaVersion";

View file

@ -26,7 +26,7 @@ for (const key in window.modules) {
} }
// Function to filter through 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 = []; const found = [];
// Get the previous moment locale // Get the previous moment locale
@ -53,7 +53,7 @@ export const filterModules = (modules: MetroModules, single = false) => (filter:
found.push(module.default); found.push(module.default);
} }
if(filter(module)) { if (filter(module)) {
if (single) return module; if (single) return module;
else found.push(module); else found.push(module);
} }

View file

@ -4,6 +4,8 @@ import { awaitSyncWrapper, createStorage, wrapSync } from "@lib/storage";
import logger from "@lib/logger"; import logger from "@lib/logger";
import Subpage from "@ui/settings/components/Subpage"; import Subpage from "@ui/settings/components/Subpage";
// TODO: Properly implement hash-based updating
type EvaledPlugin = { type EvaledPlugin = {
onLoad?(): void; onLoad?(): void;
onUnload(): void; onUnload(): void;
@ -20,7 +22,7 @@ export async function fetchPlugin(id: string, enabled = true) {
let pluginManifest: PluginManifest; let pluginManifest: PluginManifest;
try { 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 { } catch {
throw new Error(`Failed to fetch manifest for ${id}`); 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 // TODO: Remove duplicate error if possible
try { try {
// by polymanifest spec, plugins should always specify their main file, but just in case // 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 { } catch {
throw new Error(`Failed to fetch JS for ${id}`); 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) { export async function evalPlugin(plugin: Plugin) {
// TODO: Refactor to not depend on own window object
const vendettaForPlugins = { const vendettaForPlugins = {
...window.vendetta, ...window.vendetta,
plugin: { 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 plugin = plugins[id];
const pluginRet = loadedPlugins[id]; const pluginRet = loadedPlugins[id];
if (!plugin) throw new Error("Attempted to stop non-existent plugin"); if (!plugin) throw new Error("Attempted to stop non-existent plugin");
@ -102,7 +103,7 @@ export function stopPlugin(id: string) {
} }
delete loadedPlugins[id]; delete loadedPlugins[id];
plugin.enabled = false; disable && (plugin.enabled = false);
} }
export function removePlugin(id: string) { export function removePlugin(id: string) {
@ -111,15 +112,18 @@ export function removePlugin(id: string) {
delete plugins[id]; delete plugins[id];
} }
export async function initializePlugins() { export async function initPlugins() {
await awaitSyncWrapper(plugins); await awaitSyncWrapper(plugins);
const allIds = Object.keys(plugins); const allIds = Object.keys(plugins);
await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false))); await Promise.allSettled(allIds.map((pl) => fetchPlugin(pl, false)));
for (const pl of allIds.filter((pl) => plugins[pl].enabled)) for (const pl of allIds.filter((pl) => plugins[pl].enabled)) startPlugin(pl);
startPlugin(pl);
return stopAllPlugins;
} }
const stopAllPlugins = () => Object.keys(plugins).forEach(p => stopPlugin(p, false));
export const getSettings = (id: string) => loadedPlugins[id]?.settings; export const getSettings = (id: string) => loadedPlugins[id]?.settings;
export function showSettings(plugin: Plugin) { export function showSettings(plugin: Plugin) {

54
src/lib/windowObject.ts Normal file
View file

@ -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<T extends Record<string, any>>(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<VendettaObject> {
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;
}
}
}

View file

@ -1,23 +1,23 @@
import { Asset, Indexable, } from "@types"; import { Asset, Indexable } from "@types";
import { after } from "@lib/patcher"; import { after } from "@lib/patcher";
import { assets } from "@metro/common"; import { assets } from "@metro/common";
export const all: Indexable<Asset> = {}; export const all: Indexable<Asset> = {};
export function patchAssets() { export function patchAssets() {
try { const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => {
after("registerAsset", assets, (args: Asset[], id: number) => { const asset = args[0];
const asset = args[0]; all[asset.name] = { ...asset, id: id };
all[asset.name] = { ...asset, id: id }; });
});
for (let id = 1; ; id++) { for (let id = 1; ; id++) {
const asset = assets.getAssetByID(id); const asset = assets.getAssetByID(id);
if (!asset) break; if (!asset) break;
if (all[asset.name]) continue; if (all[asset.name]) continue;
all[asset.name] = { ...asset, id: id }; all[asset.name] = { ...asset, id: id };
}; };
} catch {};
return unpatch;
} }
export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter); export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter);

View file

@ -18,7 +18,7 @@ function override() {
FluxDispatcher.unsubscribe("I18N_LOAD_START", override); FluxDispatcher.unsubscribe("I18N_LOAD_START", override);
} }
export function fixTheme() { export default function fixTheme() {
try { try {
if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override); if (ThemeStore) FluxDispatcher.subscribe("I18N_LOAD_START", override);
} catch(e) { } catch(e) {

View file

@ -1,3 +1,4 @@
import { NavigationNative } from "@metro/common";
import { Forms } from "@ui/components"; import { Forms } from "@ui/components";
import { getAssetIDByName } from "@ui/assets"; import { getAssetIDByName } from "@ui/assets";
import { useProxy } from "@lib/storage"; import { useProxy } from "@lib/storage";
@ -5,11 +6,8 @@ import settings from "@lib/settings";
const { FormRow, FormSection, FormDivider } = Forms; const { FormRow, FormSection, FormDivider } = Forms;
interface SettingsSectionProps { export default function SettingsSection() {
navigation: any; const navigation = NavigationNative.useNavigation();
}
export default function SettingsSection({ navigation }: SettingsSectionProps) {
useProxy(settings); useProxy(settings);
return ( return (

View file

@ -9,10 +9,11 @@ import Developer from "@ui/settings/pages/Developer";
const screensModule = findByDisplayName("getScreens", false); const screensModule = findByDisplayName("getScreens", false);
const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false); const settingsModule = findByDisplayName("UserSettingsOverviewWrapper", false);
let prevPatches: Function[] = [];
export default function initSettings() { export default function initSettings() {
after("default", screensModule, (args, existingScreens) => { const patches = new Array<Function>;
patches.push(after("default", screensModule, (args, existingScreens) => {
return { return {
...existingScreens, ...existingScreens,
VendettaSettings: { VendettaSettings: {
@ -28,24 +29,23 @@ export default function initSettings() {
render: Developer 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"); const Overview = findInReactTree(ret.props.children, i => i.type && i.type.name === "UserSettingsOverview");
// Upload logs button gone // 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"); const index = children.findIndex((c: any) => c?.type?.name === "UploadLogsButton");
if (index !== -1) children.splice(index, 1); 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 titles = [i18n.Messages["BILLING_SETTINGS"], i18n.Messages["PREMIUM_SETTINGS"]];
const index = children.findIndex((c: any) => titles.includes(c.props.title)); const index = children.findIndex((c: any) => titles.includes(c.props.title));
children.splice(index === -1 ? 4 : index, 0, <SettingsSection navigation={Overview.props.navigation} />); children.splice(index === -1 ? 4 : index, 0, <SettingsSection />);
})); }));
}); }, true);
return () => patches.forEach(p => p());
} }