feat!: totally bunnify plugins

This commit is contained in:
amsyarasyiq 2024-07-09 15:20:17 +08:00
parent 4d144f60d9
commit 64473cdadf
7 changed files with 54 additions and 64 deletions

View file

@ -19,6 +19,7 @@ import * as color from "@ui/color";
import * as components from "@ui/components";
import { createThemedStyleSheet } from "@ui/styles";
import * as toasts from "@ui/toasts";
import { omit } from "es-toolkit";
import { createElement, useEffect } from "react";
import { View } from "react-native";
@ -132,7 +133,7 @@ export const initVendettaObject = (): any => {
findInTree: (tree: any, filter: any, options: any) => utils.findInTree(tree, filter, options),
safeFetch: (input: RequestInfo | URL, options?: RequestInit | undefined, timeout?: number | undefined) => utils.safeFetch(input, options, timeout),
unfreeze: (obj: object) => Object.isFrozen(obj) ? ({ ...obj }) : obj,
without: (object: any, ...keys: any) => utils.without(object, ...keys)
without: (object: any, ...keys: any) => omit(object, keys)
},
debug: {
connectToDebugger: (url: string) => debug.connectToDebugger(url),
@ -173,10 +174,10 @@ export const initVendettaObject = (): any => {
plugins: plugins.sourceStore,
fetchPlugin: (source: string) => plugins.fetchAndStorePlugin(source),
installPlugin: (source: string, enabled = true) => plugins.installPlugin(source, enabled),
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.pluginId),
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.pluginId, disable),
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.pluginId),
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.pluginId)
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceStore[id]!.id),
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceStore[id]!.id, disable),
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceStore[id]!.id),
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceStore[id]!.id)
},
themes: {
themes: themes.themes,

View file

@ -2,7 +2,7 @@ import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
import { useProxy } from "@lib/api/storage";
import { BunnyPlugin, installPlugin, selectedSources, sourceStore } from "@lib/managers/plugins";
import { BunnyPlugin, installPlugin, preferredSourceStore, sourceStore } from "@lib/managers/plugins";
import { settings } from "@lib/settings";
import { Author } from "@lib/utils/types";
@ -17,7 +17,7 @@ export default function Plugins() {
"manifest.description",
p => p.manifest.authors?.map((a: Author) => a.name).join()
]}
items={selectedSources}
items={preferredSourceStore}
resolveItem={source => sourceStore[source]}
fetchFunction={installPlugin}
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}

2
src/global.d.ts vendored
View file

@ -1,5 +1,5 @@
declare global {
var React: typeof import("react");
type React = typeof import("react");
var SkiaApi: typeof import("@shopify/react-native-skia").Skia;
interface Window {

View file

@ -1,12 +1,13 @@
import { allSettled } from "@core/polyfills/allSettled";
import { createVdPluginObject } from "@core/polyfills/vendettaObject";
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
import { settings } from "@lib/settings";
import { safeFetch } from "@lib/utils";
import { BUNNY_PROXY_PREFIX, OLD_BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import invariant from "@lib/utils/invariant";
import { proxyLazy } from "@lib/utils/lazy";
import { logger } from "@lib/utils/logger";
import { Author } from "@lib/utils/types";
import { isNotNil, uniqWith } from "es-toolkit";
type EvaledPlugin = {
onLoad?(): void;
@ -15,6 +16,7 @@ type EvaledPlugin = {
};
interface PluginManifest {
id: string;
name: string;
description: string;
authors: Author[];
@ -28,8 +30,8 @@ interface PluginManifest {
}
export interface BunnyPlugin {
id: string;
source: string;
pluginId: string;
manifest: PluginManifest;
enabled: boolean;
update: boolean;
@ -40,43 +42,27 @@ export interface BunnyPlugin {
// TODO: Use fs to avoid unnecessary memory usage
js: string;
/**
* @deprecated use `source` (for URL) or `pluginId` (plugin's unique ID)
* */
id: string;
}
const pluginsEnabled = () => !settings.safeMode?.enabled;
const arePluginsEnabled = () => !settings.safeMode?.enabled;
const _pluginInstances: Record<string, EvaledPlugin> = {};
export const selectedSources = wrapSync(createStorage<Record<string, string | undefined>>(createMMKVBackend("SELECTED_PLUGIN_SOURCES")));
export const sourceStore = proxyLazy(() => {
type StorageInterface = Record<string, BunnyPlugin | undefined>;
const storagePromise = createStorage<StorageInterface>(createMMKVBackend("VENDETTA_PLUGINS"));
storagePromise.then(st => {
for (const plugin of Object.values(st)) if (plugin) {
plugin.pluginId ??= `${plugin.manifest.name}-${plugin.manifest.authors?.[0]?.name}`.toLowerCase();
plugin.source ??= plugin.id;
}
});
return wrapSync(storagePromise);
});
export const sourceStore = wrapSync(createStorage<{ [id in string]?: BunnyPlugin }>(createMMKVBackend("PLUGIN_SOURCES_STORE")));
export const preferredSourceStore = wrapSync(createStorage<{ [id in string]?: string }>(createMMKVBackend("PREFERRED_PLUGIN_SOURCE")));
export function getPluginById(id: string) {
if (!selectedSources[id]) {
if (!id) return undefined;
if (!preferredSourceStore[id]) {
for (const plugin of Object.values(sourceStore)) {
if (plugin?.pluginId === id) {
selectedSources[id] = plugin.source;
break;
if (plugin?.id === id) {
return plugin;
}
}
}
return selectedSources[id] ? sourceStore[selectedSources[id]!] : undefined;
return sourceStore[preferredSourceStore[id]!];
}
export async function fetchAndStorePlugin(source: string) {
@ -98,21 +84,22 @@ export async function fetchAndStorePlugin(source: string) {
throw new Error(`Failed to fetch manifest for ${source}`);
}
for (const f of ["id", "main", "hash", "bunny"] as const)
invariant(pluginManifest[f], `Plugin manifest does not contain mandatory field: '${f}'`);
let pluginJs: string | undefined;
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
try {
// by polymanifest spec, plugins should always specify their main file, but just in case
pluginJs = await (await fetch(source + (pluginManifest.main || "index.js"))).text();
pluginJs = await (await fetch(source + pluginManifest.main)).text();
} catch { } // Empty catch, checked below
}
invariant(pluginJs || existingPlugin, `Failed to fetch JS from ${source}`);
return sourceStore[source] = {
id: source,
id: pluginManifest.id,
source: source,
pluginId: pluginManifest.name,
manifest: pluginManifest,
enabled: existingPlugin?.enabled ?? false,
update: existingPlugin?.update ?? true,
@ -126,7 +113,7 @@ export async function installPlugin(source: string, enabled = true) {
invariant(!(source in sourceStore), "Source was already installed");
const plugin = await fetchAndStorePlugin(source);
if (enabled) await startPlugin(plugin.pluginId);
if (enabled) await startPlugin(plugin.id);
}
/**
@ -146,7 +133,7 @@ export async function startPlugin(id: string) {
invariant(plugin, "Attempted to start non-existent plugin");
try {
if (pluginsEnabled()) {
if (arePluginsEnabled()) {
const pluginRet: EvaledPlugin = await evalPlugin(plugin);
_pluginInstances[id] = pluginRet;
pluginRet.onLoad?.();
@ -174,7 +161,7 @@ export function stopPlugin(id: string, disable = true) {
const pluginInstance = _pluginInstances[id];
invariant(plugin, "Attempted to stop non-existent plugin");
if (pluginsEnabled()) {
if (arePluginsEnabled()) {
try {
pluginInstance?.onUnload?.();
} catch (e) {
@ -199,30 +186,38 @@ export async function removePlugin(id: string) {
* @internal
*/
export async function initPlugins() {
await awaitStorage(sourceStore, selectedSources, settings);
await awaitStorage(sourceStore, preferredSourceStore, settings);
// TODO: Optimize this
if (pluginsEnabled()) {
const allIds = [...new Set(Object.values(sourceStore).map(s => s!.pluginId))];
if (arePluginsEnabled()) {
const plugins = uniqWith(
Object.values(sourceStore)
.map(s => getPluginById(s!.id))
.filter(isNotNil),
(a, b) => a?.id === b?.id
);
// Loop over enabled plugins, update and start them
const enabledPlugins = allIds.filter(id => getPluginById(id)?.enabled);
await Promise.all(enabledPlugins.map(async pl => {
if (getPluginById(pl)!.update) {
const updatePromise: Promise<unknown>[] = [];
const updateAndStart = async (plugin: BunnyPlugin) => {
if (plugin.update) {
try {
await fetchAndStorePlugin(pl);
await fetchAndStorePlugin(plugin.id);
} catch (e) {
logger.error(e);
}
}
await startPlugin(pl);
}));
await startPlugin(plugin.id);
};
// Loop over disabled plugins that are allowed to update
allIds
.filter(id => !getPluginById(id)?.enabled && getPluginById(id)!.update)
.forEach(id => fetchAndStorePlugin(id));
for (const plugin of plugins) {
if (plugin.enabled) {
updatePromise.push(updateAndStart(plugin));
} else if (plugin.update) {
fetchAndStorePlugin(plugin.id);
}
}
await allSettled(updatePromise);
}
return () => Object.keys(_pluginInstances).forEach(p => stopPlugin(p, false));

View file

@ -7,4 +7,3 @@ export { findInTree } from "@lib/utils/findInTree";
export * as logger from "@lib/utils/logger";
export { safeFetch } from "@lib/utils/safeFetch";
export * as types from "@lib/utils/types";
export { without } from "@lib/utils/without";

View file

@ -1,5 +0,0 @@
export function without<O extends object, K extends readonly (keyof O)[]>(object: O, ...keys: K): Omit<O, typeof keys[number]> {
const cloned = { ...object };
keys.forEach(k => delete cloned[k]);
return cloned;
}

View file

@ -24,7 +24,7 @@
"noEmit": true,
"allowJs": true,
"checkJs": true,
"jsx": "react",
"jsx": "react-jsx",
"target": "esnext"
}
}