GoonCord/src/lib/managers/plugins.ts

238 lines
7.3 KiB
TypeScript
Raw Normal View History

2024-05-23 20:27:16 +08:00
import { createVdPluginObject } from "@core/polyfills/vendettaObject";
2024-06-18 02:19:31 +08:00
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
2024-03-06 01:57:59 +08:00
import { settings } from "@lib/settings";
2023-04-15 19:54:23 +01:00
import { safeFetch } from "@lib/utils";
2024-06-18 02:19:31 +08:00
import { VD_PROXY_PREFIX } from "@lib/utils/constants";
import invariant from "@lib/utils/invariant";
import { proxyLazy } from "@lib/utils/lazy";
2024-05-23 20:27:16 +08:00
import { logger } from "@lib/utils/logger";
2024-03-06 01:57:59 +08:00
import { Author } from "@lib/utils/types";
2023-01-03 08:05:16 +00:00
type EvaledPlugin = {
onLoad?(): void;
onUnload(): void;
settings: () => JSX.Element;
2023-01-03 08:05:16 +00:00
};
2024-06-18 02:19:31 +08:00
interface PluginManifest {
2024-02-23 15:11:35 +08:00
name: string;
description: string;
authors: Author[];
main: string;
hash: string;
// Vendor-specific field, contains our own data
vendetta?: {
icon?: string;
};
2024-06-18 02:19:31 +08:00
bunny?: {};
2024-02-23 15:11:35 +08:00
}
2024-02-23 19:30:23 +08:00
export interface BunnyPlugin {
2024-06-18 02:19:31 +08:00
source: string;
pluginId: string;
2024-02-23 15:11:35 +08:00
manifest: PluginManifest;
enabled: boolean;
update: boolean;
2024-06-18 02:19:31 +08:00
/**
* Message of plugin startup error. Gone after plugin boots successfully
* */
2024-05-23 21:35:53 +08:00
error?: string;
2024-06-25 02:28:04 +08:00
2024-06-18 02:19:31 +08:00
// TODO: Use fs to avoid unnecessary memory usage
2024-02-23 15:11:35 +08:00
js: string;
2024-06-18 02:19:31 +08:00
/**
* @deprecated use `source` (for URL) or `pluginId` (plugin's unique ID)
* */
id: string;
2024-02-23 15:11:35 +08:00
}
2024-06-25 02:28:04 +08:00
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
2024-06-18 02:19:31 +08:00
const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
const pluginsEnabled = () => !settings.safeMode?.enabled;
const _pluginInstances: Record<string, EvaledPlugin> = {};
2024-06-25 02:28:04 +08:00
export const selectedSources = wrapSync(createStorage<Record<string, string | undefined>>(createMMKVBackend("SELECTED_PLUGIN_SOURCES")));
export const sourceStore = proxyLazy(() => {
2024-06-18 02:19:31 +08:00
type StorageInterface = Record<string, BunnyPlugin | undefined>;
const storagePromise = createStorage<StorageInterface>(createMMKVBackend("VENDETTA_PLUGINS"));
2023-01-07 23:05:14 +00:00
2024-06-18 02:19:31 +08:00
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 function getPluginById(id: string) {
2024-06-25 02:28:04 +08:00
if (!selectedSources[id]) {
for (const plugin of Object.values(sourceStore)) {
2024-06-18 02:19:31 +08:00
if (plugin?.pluginId === id) {
2024-06-25 02:28:04 +08:00
selectedSources[id] = plugin.source;
2024-06-18 02:19:31 +08:00
break;
}
}
}
2024-06-25 02:28:04 +08:00
return selectedSources[id] ? sourceStore[selectedSources[id]!] : undefined;
}
2024-06-25 02:28:04 +08:00
export async function fetchAndStorePlugin(source: string) {
2024-06-18 02:19:31 +08:00
if (!source.endsWith("/")) source += "/";
2024-06-25 02:28:04 +08:00
const existingPlugin = sourceStore[source];
2024-06-18 02:19:31 +08:00
const fetch = (url: string) => safeFetch(
url
.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX)
.replace(OLD_BUNNY_PROXY_PREFIX, BUNNY_PROXY_PREFIX),
{ cache: "no-store" }
);
let pluginManifest: PluginManifest;
try {
2024-06-18 02:19:31 +08:00
pluginManifest = await (await fetch(source + "manifest.json")).json();
} catch {
2024-06-18 02:19:31 +08:00
throw new Error(`Failed to fetch manifest for ${source}`);
}
let pluginJs: string | undefined;
2023-01-03 08:05:16 +00:00
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
try {
// by polymanifest spec, plugins should always specify their main file, but just in case
2024-06-18 02:19:31 +08:00
pluginJs = await (await fetch(source + (pluginManifest.main || "index.js"))).text();
2024-03-06 01:57:59 +08:00
} catch { } // Empty catch, checked below
}
2023-01-04 08:01:56 +00:00
2024-06-18 02:19:31 +08:00
invariant(pluginJs || existingPlugin, `Failed to fetch JS from ${source}`);
2023-02-19 22:35:53 +00:00
2024-06-25 02:28:04 +08:00
return sourceStore[source] = {
2024-06-18 02:19:31 +08:00
id: source,
source: source,
pluginId: pluginManifest.name,
manifest: pluginManifest,
enabled: existingPlugin?.enabled ?? false,
update: existingPlugin?.update ?? true,
2024-06-18 02:19:31 +08:00
js: pluginJs ?? existingPlugin!.js,
2024-05-23 21:35:53 +08:00
error: existingPlugin?.error
};
}
2023-01-30 00:40:56 +00:00
2024-06-18 02:19:31 +08:00
export async function installPlugin(source: string, enabled = true) {
if (!source.endsWith("/")) source += "/";
2024-06-25 02:28:04 +08:00
invariant(!(source in sourceStore), "Source was already installed");
2024-06-18 02:19:31 +08:00
2024-06-25 02:28:04 +08:00
const plugin = await fetchAndStorePlugin(source);
2024-06-18 02:19:31 +08:00
if (enabled) await startPlugin(plugin.pluginId);
2023-01-03 08:05:16 +00:00
}
2024-03-11 15:04:11 +08:00
/**
* @internal
*/
2024-06-18 02:19:31 +08:00
async function evalPlugin(plugin: BunnyPlugin) {
2024-05-23 20:27:16 +08:00
const vdObject = await createVdPluginObject(plugin);
2024-06-18 02:19:31 +08:00
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.source}?hash=${plugin.manifest.hash}`;
2023-01-03 08:05:16 +00:00
2024-05-23 20:27:16 +08:00
const raw = (0, eval)(pluginString)(vdObject);
2024-03-06 01:57:59 +08:00
const ret = typeof raw === "function" ? raw() : raw;
2023-04-16 14:02:22 +01:00
return ret?.default ?? ret ?? {};
2023-01-03 08:05:16 +00:00
}
export async function startPlugin(id: string) {
2024-06-18 02:19:31 +08:00
const plugin = getPluginById(id);
invariant(plugin, "Attempted to start non-existent plugin");
2023-01-03 08:05:16 +00:00
try {
2024-06-18 02:19:31 +08:00
if (pluginsEnabled()) {
2024-03-11 15:04:11 +08:00
const pluginRet: EvaledPlugin = await evalPlugin(plugin);
2024-06-18 02:19:31 +08:00
_pluginInstances[id] = pluginRet;
pluginRet.onLoad?.();
}
2024-05-23 21:35:53 +08:00
delete plugin.error;
2023-01-03 08:05:16 +00:00
plugin.enabled = true;
} catch (e) {
2024-06-18 02:19:31 +08:00
logger.error(`Plugin ${plugin.source} errored whilst loading, and will be unloaded`, e);
2024-05-23 21:35:53 +08:00
plugin.error = e instanceof Error ? e.stack : String(e);
2023-01-03 08:05:16 +00:00
try {
2024-06-18 02:19:31 +08:00
_pluginInstances[id]?.onUnload?.();
} catch (e2) {
2024-06-18 02:19:31 +08:00
logger.error(`Plugin ${plugin.source} errored whilst unloading`, e2);
2023-01-03 08:05:16 +00:00
}
2024-06-18 02:19:31 +08:00
delete _pluginInstances[id];
2023-01-03 08:05:16 +00:00
plugin.enabled = false;
}
}
export function stopPlugin(id: string, disable = true) {
2024-06-18 02:19:31 +08:00
const plugin = getPluginById(id);
const pluginInstance = _pluginInstances[id];
invariant(plugin, "Attempted to stop non-existent plugin");
2023-01-03 08:05:16 +00:00
2024-06-18 02:19:31 +08:00
if (pluginsEnabled()) {
try {
2024-06-18 02:19:31 +08:00
pluginInstance?.onUnload?.();
} catch (e) {
2024-06-18 02:19:31 +08:00
logger.error(`Plugin ${plugin.source} errored whilst unloading`, e);
}
2024-06-18 02:19:31 +08:00
delete _pluginInstances[id];
2023-01-03 08:05:16 +00:00
}
2024-06-18 02:19:31 +08:00
if (disable) plugin.enabled = false;
2023-01-03 08:05:16 +00:00
}
export async function removePlugin(id: string) {
2024-06-18 02:19:31 +08:00
const plugin = getPluginById(id);
invariant(plugin, "Removing non-existent plugin");
if (plugin.enabled) stopPlugin(id);
2024-06-25 02:28:04 +08:00
delete sourceStore[plugin.source];
2024-06-18 02:19:31 +08:00
await purgeStorage(plugin.source);
2023-01-07 23:05:14 +00:00
}
2024-03-11 15:04:11 +08:00
/**
* @internal
*/
export async function initPlugins() {
2024-06-25 02:28:04 +08:00
await awaitStorage(sourceStore, selectedSources, settings);
2024-06-18 02:19:31 +08:00
// TODO: Optimize this
if (pluginsEnabled()) {
2024-06-25 02:28:04 +08:00
const allIds = [...new Set(Object.values(sourceStore).map(s => s!.pluginId))];
2024-06-18 02:19:31 +08:00
// 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) {
try {
2024-06-25 02:28:04 +08:00
await fetchAndStorePlugin(pl);
2024-06-18 02:19:31 +08:00
} catch (e) {
logger.error(e);
}
}
2024-06-25 02:28:04 +08:00
2024-06-18 02:19:31 +08:00
await startPlugin(pl);
}));
// Loop over disabled plugins that are allowed to update
allIds
.filter(id => !getPluginById(id)?.enabled && getPluginById(id)!.update)
2024-06-25 02:28:04 +08:00
.forEach(id => fetchAndStorePlugin(id));
2024-03-06 01:57:59 +08:00
}
2024-06-18 02:19:31 +08:00
return () => Object.keys(_pluginInstances).forEach(p => stopPlugin(p, false));
2023-01-30 14:59:47 +01:00
}
2024-06-18 02:19:31 +08:00
export function getSettingsComponent(id: string) {
return _pluginInstances[id]?.settings;
}