[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 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();

View file

@ -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

View file

@ -35,13 +35,15 @@ 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] }));
}
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";

View file

@ -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);
}

View file

@ -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) {

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 { assets } from "@metro/common";
export const all: Indexable<Asset> = {};
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);

View file

@ -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) {

View file

@ -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 (

View file

@ -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<Function>;
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, <SettingsSection navigation={Overview.props.navigation} />);
children.splice(index === -1 ? 4 : index, 0, <SettingsSection />);
}));
});
}, true);
return () => patches.forEach(p => p());
}