feat(plugins): init new bunny spec
This commit is contained in:
parent
dc4d3a18c8
commit
b3c1b748a0
14 changed files with 209 additions and 109 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types";
|
||||
import { plugins as pluginStorage } from "@lib/managers/plugins";
|
||||
import { BunnyPlugin, sourceToObject as pluginStorage } from "@lib/managers/plugins";
|
||||
import { messageUtil } from "@metro/common";
|
||||
|
||||
export default () => <ApplicationCommand>{
|
||||
|
|
@ -15,7 +15,8 @@ export default () => <ApplicationCommand>{
|
|||
}
|
||||
],
|
||||
execute([ephemeral], ctx) {
|
||||
const plugins = Object.values(pluginStorage).sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
||||
const plugins = Object.values(pluginStorage).filter(Boolean) as unknown as BunnyPlugin[];
|
||||
plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
||||
|
||||
const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name);
|
||||
const disabled = plugins.filter(p => !p.enabled).map(p => p.manifest.name);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { requireAssetIndex } from "@lib/api/assets";
|
|||
import { isThemeSupported } from "@lib/api/native/loader";
|
||||
import { after } from "@lib/api/patcher";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { installPlugin, plugins, removePlugin } from "@lib/managers/plugins";
|
||||
import { installPlugin, sourceToObject, removePlugin } from "@lib/managers/plugins";
|
||||
import { installTheme, removeTheme, themes } from "@lib/managers/themes";
|
||||
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, PROXY_PREFIX, THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { Button } from "@metro/common/components";
|
||||
import { findByProps, findByPropsProxy } from "@metro/utils";
|
||||
|
|
@ -21,8 +21,8 @@ const forumReactions = findByPropsProxy("MostCommonForumPostReaction");
|
|||
|
||||
const postMap = {
|
||||
Plugin: {
|
||||
storage: plugins,
|
||||
urlsFilter: (url: string) => url.startsWith(PROXY_PREFIX),
|
||||
storage: sourceToObject,
|
||||
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
|
||||
installOrRemove: (url: string) => {
|
||||
const isInstalled = postMap.Plugin.storage[url];
|
||||
return isInstalled ? removePlugin(url) : installPlugin(url);
|
||||
|
|
@ -63,7 +63,7 @@ function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet
|
|||
function useInstaller(thread: any, firstMessage = null, actionSheet = false): [true] | [false, PostType, boolean, boolean, () => Promise<void>] {
|
||||
const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? [];
|
||||
|
||||
useProxy(plugins);
|
||||
useProxy(sourceToObject);
|
||||
useProxy(themes);
|
||||
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { isThemeSupported } from "@lib/api/native/loader";
|
|||
import { after, instead } from "@lib/api/patcher";
|
||||
import { installPlugin } from "@lib/managers/plugins";
|
||||
import { installTheme } from "@lib/managers/themes";
|
||||
import { PROXY_PREFIX, THEMES_CHANNEL_ID } from "@lib/utils/constants";
|
||||
import { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { channels, url } from "@metro/common";
|
||||
import { byMutableProp } from "@metro/filters";
|
||||
|
|
@ -20,7 +20,7 @@ const { getChannelId } = lazyDestructure(() => channels);
|
|||
const { getChannel } = lazyDestructure(() => findByProps("getChannel"));
|
||||
|
||||
function typeFromUrl(url: string) {
|
||||
if (url.startsWith(PROXY_PREFIX)) {
|
||||
if (url.startsWith(VD_PROXY_PREFIX)) {
|
||||
return "plugin";
|
||||
} else if (url.endsWith(".json") && isThemeSupported()) {
|
||||
return "theme";
|
||||
|
|
|
|||
|
|
@ -170,13 +170,13 @@ export const initVendettaObject = (): any => {
|
|||
rawColors: color.rawColors
|
||||
},
|
||||
plugins: {
|
||||
plugins: plugins.plugins,
|
||||
fetchPlugin: (id: string) => plugins.fetchPlugin(id),
|
||||
installPlugin: (id: string, enabled?: boolean | undefined) => plugins.installPlugin(id, enabled),
|
||||
startPlugin: (id: string) => plugins.startPlugin(id),
|
||||
stopPlugin: (id: string, disable?: boolean | undefined) => plugins.stopPlugin(id, disable),
|
||||
removePlugin: (id: string) => plugins.removePlugin(id),
|
||||
getSettings: (id: string) => plugins.getSettings(id)
|
||||
plugins: plugins.sourceToObject,
|
||||
fetchPlugin: (source: string) => plugins.fetchPlugin(source),
|
||||
installPlugin: (source: string, enabled = true) => plugins.installPlugin(source, enabled),
|
||||
startPlugin: (id: string) => plugins.startPlugin(plugins.sourceToObject[id]!.pluginId),
|
||||
stopPlugin: (id: string, disable = true) => plugins.stopPlugin(plugins.sourceToObject[id]!.pluginId, disable),
|
||||
removePlugin: (id: string) => plugins.removePlugin(plugins.sourceToObject[id]!.pluginId),
|
||||
getSettings: (id: string) => plugins.getSettingsComponent(plugins.sourceToObject[id]!.pluginId)
|
||||
},
|
||||
themes: {
|
||||
themes: themes.themes,
|
||||
|
|
@ -195,7 +195,7 @@ export const initVendettaObject = (): any => {
|
|||
useProxy: (_storage: any) => storage.useProxy(_storage),
|
||||
createStorage: (backend: any) => storage.createStorage(backend),
|
||||
wrapSync: (store: any) => storage.wrapSync(store),
|
||||
awaitSyncWrapper: (store: any) => storage.awaitSyncWrapper(store),
|
||||
awaitSyncWrapper: (store: any) => storage.awaitStorage(store),
|
||||
createMMKVBackend: (store: string) => storage.createMMKVBackend(store),
|
||||
createFileBackend: (file: string) => {
|
||||
// Redirect path to vendetta_theme.json
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
|
|||
interface AddonPageProps<T> {
|
||||
title: string;
|
||||
fetchFunction: (url: string) => Promise<void>;
|
||||
items: Record<string, T & ({ id: string; } | { name: string; })>;
|
||||
items: Record<string, any>;
|
||||
resolveItem?: (value: any) => T | undefined;
|
||||
safeModeMessage: string;
|
||||
safeModeExtras?: JSX.Element | JSX.Element[];
|
||||
card: React.ComponentType<CardWrapper<T>>;
|
||||
|
|
@ -36,7 +37,9 @@ export default function AddonPage<T>({ card: CardComponent, ...props }: AddonPag
|
|||
const [search, setSearch] = React.useState("");
|
||||
|
||||
const items = useMemo(() => {
|
||||
return Object.values(props.items).filter(i => typeof i === "object");
|
||||
let values = Object.values(props.items);
|
||||
if (props.resolveItem) values = values.map(props.resolveItem);
|
||||
return values.filter(i => i && typeof i === "object");
|
||||
}, [props.items]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { requireAssetIndex } from "@lib/api/assets";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { BunnyPlugin, getSettings, startPlugin, stopPlugin } from "@lib/managers/plugins";
|
||||
import { BunnyPlugin, getSettingsComponent, startPlugin, stopPlugin } from "@lib/managers/plugins";
|
||||
import { showSheet } from "@lib/ui/sheets";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { findByProps } from "@metro";
|
||||
|
|
@ -39,20 +39,31 @@ function Title() {
|
|||
const iconName = plugin.manifest.vendetta?.icon;
|
||||
const icon = iconName && requireAssetIndex(iconName);
|
||||
|
||||
return <Text
|
||||
numberOfLines={1}
|
||||
variant="heading-lg/semibold"
|
||||
const textElement = (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
variant="heading-lg/semibold"
|
||||
>
|
||||
{plugin.manifest.name}
|
||||
</Text>
|
||||
);
|
||||
|
||||
if (!icon) return textElement;
|
||||
|
||||
return <View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
{icon && <>
|
||||
{/* TODO: Image appears invisible for some devices/installs */}
|
||||
<Image
|
||||
style={styles.smallIcon}
|
||||
source={icon}
|
||||
/>
|
||||
{" "}
|
||||
</>}
|
||||
{plugin.manifest.name}
|
||||
</Text>;
|
||||
<Image
|
||||
style={styles.smallIcon}
|
||||
source={icon}
|
||||
/>
|
||||
{textElement}
|
||||
</View>;
|
||||
}
|
||||
|
||||
// TODO: Wrap in a Card-ish component with red bg
|
||||
|
|
@ -99,10 +110,10 @@ export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
icon={requireAssetIndex("WrenchIcon")}
|
||||
disabled={!getSettings(plugin.id)}
|
||||
disabled={!getSettingsComponent(plugin.id)}
|
||||
onPress={() => navigation.push("VendettaCustomPage", {
|
||||
title: plugin.manifest.name,
|
||||
render: getSettings(plugin.id),
|
||||
render: getSettingsComponent(plugin.id),
|
||||
})}
|
||||
/>
|
||||
<IconButton
|
||||
|
|
|
|||
|
|
@ -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, plugins } from "@lib/managers/plugins";
|
||||
import { BunnyPlugin, idToSource, installPlugin, sourceToObject } from "@lib/managers/plugins";
|
||||
import { settings } from "@lib/settings";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
||||
|
|
@ -17,8 +17,9 @@ export default function Plugins() {
|
|||
"manifest.description",
|
||||
p => p.manifest.authors?.map((a: Author) => a.name).join()
|
||||
]}
|
||||
items={idToSource}
|
||||
resolveItem={id => sourceToObject[id]}
|
||||
fetchFunction={installPlugin}
|
||||
items={plugins}
|
||||
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
|
||||
card={PluginCard}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { requireAssetIndex } from "@lib/api/assets";
|
||||
import { purgeStorage, useProxy } from "@lib/api/storage";
|
||||
import { BunnyPlugin, fetchPlugin, getSettings, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
|
||||
import { BunnyPlugin, fetchPlugin, getSettingsComponent, removePlugin, startPlugin, stopPlugin } from "@lib/managers/plugins";
|
||||
import { showConfirmationAlert } from "@lib/ui/alerts";
|
||||
import { hideSheet } from "@lib/ui/sheets";
|
||||
import { showToast } from "@lib/ui/toasts";
|
||||
|
|
@ -18,7 +18,7 @@ interface InfoProps {
|
|||
export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps) {
|
||||
useProxy(plugin);
|
||||
|
||||
const Settings = getSettings(plugin.id);
|
||||
const Settings = getSettingsComponent(plugin.id);
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,11 @@ export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
|
|||
});
|
||||
}
|
||||
|
||||
export const awaitSyncWrapper = (store: any) => new Promise<void>(res => store[syncAwaitSymbol](res));
|
||||
export function awaitStorage(...stores: any[]) {
|
||||
return Promise.all(
|
||||
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
createFileBackend,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
|
||||
import { awaitSyncWrapper, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
|
||||
type FontMap = Record<string, string>;
|
||||
|
|
@ -107,7 +107,7 @@ export async function removeFont(name: string) {
|
|||
}
|
||||
|
||||
export async function updateFonts() {
|
||||
await awaitSyncWrapper(fonts);
|
||||
await awaitStorage(fonts);
|
||||
await Promise.allSettled(
|
||||
Object.keys(fonts).map(
|
||||
name => saveFont(fonts[name], fonts.__selected === name)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { allSettled } from "@core/polyfills/allSettled";
|
||||
import { createVdPluginObject } from "@core/polyfills/vendettaObject";
|
||||
import { awaitSyncWrapper, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { settings } from "@lib/settings";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { BUNNY_PROXY_PREFIX, PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { 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";
|
||||
|
||||
|
|
@ -13,8 +14,7 @@ type EvaledPlugin = {
|
|||
settings: () => JSX.Element;
|
||||
};
|
||||
|
||||
// See https://github.com/vendetta-mod/polymanifest
|
||||
export interface PluginManifest {
|
||||
interface PluginManifest {
|
||||
name: string;
|
||||
description: string;
|
||||
authors: Author[];
|
||||
|
|
@ -24,38 +24,81 @@ export interface PluginManifest {
|
|||
vendetta?: {
|
||||
icon?: string;
|
||||
};
|
||||
bunny?: {};
|
||||
}
|
||||
|
||||
export interface BunnyPlugin {
|
||||
id: string;
|
||||
source: string;
|
||||
pluginId: string;
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
update: boolean;
|
||||
/**
|
||||
* Message of plugin startup error. Gone after plugin boots successfully
|
||||
* */
|
||||
error?: string;
|
||||
// TODO: Use fs to avoid unnecessary memory usage
|
||||
js: string;
|
||||
|
||||
/**
|
||||
* @deprecated use `source` (for URL) or `pluginId` (plugin's unique ID)
|
||||
* */
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const plugins = wrapSync(createStorage<Record<string, BunnyPlugin>>(createMMKVBackend("VENDETTA_PLUGINS")));
|
||||
const loadedPlugins: Record<string, EvaledPlugin> = {};
|
||||
const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
|
||||
const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
|
||||
|
||||
async function pluginFetch(url: string) {
|
||||
if (url.startsWith(PROXY_PREFIX)) {
|
||||
url = url.replace(PROXY_PREFIX, BUNNY_PROXY_PREFIX);
|
||||
const pluginsEnabled = () => !settings.safeMode?.enabled;
|
||||
|
||||
const _pluginInstances: Record<string, EvaledPlugin> = {};
|
||||
|
||||
export const idToSource = wrapSync(createStorage<Record<string, string | undefined>>(createMMKVBackend("SELECTED_PLUGINS")));
|
||||
|
||||
export const sourceToObject = 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 function getPluginById(id: string) {
|
||||
if (!idToSource[id]) {
|
||||
for (const plugin of Object.values(sourceToObject)) {
|
||||
if (plugin?.pluginId === id) {
|
||||
idToSource[id] = plugin.source;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await safeFetch(url, { cache: "no-store" });
|
||||
return idToSource[id] ? sourceToObject[idToSource[id]!] : undefined;
|
||||
}
|
||||
|
||||
export async function fetchPlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const existingPlugin = plugins[id];
|
||||
export async function fetchPlugin(source: string) {
|
||||
if (!source.endsWith("/")) source += "/";
|
||||
const existingPlugin = sourceToObject[source];
|
||||
|
||||
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 {
|
||||
pluginManifest = await (await pluginFetch(id + "manifest.json")).json();
|
||||
pluginManifest = await (await fetch(source + "manifest.json")).json();
|
||||
} catch {
|
||||
throw new Error(`Failed to fetch manifest for ${id}`);
|
||||
throw new Error(`Failed to fetch manifest for ${source}`);
|
||||
}
|
||||
|
||||
let pluginJs: string | undefined;
|
||||
|
|
@ -63,35 +106,38 @@ export async function fetchPlugin(id: string) {
|
|||
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
|
||||
try {
|
||||
// by polymanifest spec, plugins should always specify their main file, but just in case
|
||||
pluginJs = await (await pluginFetch(id + (pluginManifest.main || "index.js"))).text();
|
||||
pluginJs = await (await fetch(source + (pluginManifest.main || "index.js"))).text();
|
||||
} catch { } // Empty catch, checked below
|
||||
}
|
||||
|
||||
if (!pluginJs && !existingPlugin) throw new Error(`Failed to fetch JS for ${id}`);
|
||||
invariant(pluginJs || existingPlugin, `Failed to fetch JS from ${source}`);
|
||||
|
||||
plugins[id] = {
|
||||
id: id,
|
||||
return sourceToObject[source] = {
|
||||
id: source,
|
||||
source: source,
|
||||
pluginId: pluginManifest.name,
|
||||
manifest: pluginManifest,
|
||||
enabled: existingPlugin?.enabled ?? false,
|
||||
update: existingPlugin?.update ?? true,
|
||||
js: pluginJs ?? existingPlugin.js,
|
||||
js: pluginJs ?? existingPlugin!.js,
|
||||
error: existingPlugin?.error
|
||||
};
|
||||
}
|
||||
|
||||
export async function installPlugin(id: string, enabled = true) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
if (typeof id !== "string" || id in plugins) throw new Error("Plugin already installed");
|
||||
await fetchPlugin(id);
|
||||
if (enabled) await startPlugin(id);
|
||||
export async function installPlugin(source: string, enabled = true) {
|
||||
if (!source.endsWith("/")) source += "/";
|
||||
invariant(!(source in sourceToObject), "Source was already installed");
|
||||
|
||||
const plugin = await fetchPlugin(source);
|
||||
if (enabled) await startPlugin(plugin.pluginId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export async function evalPlugin(plugin: BunnyPlugin) {
|
||||
async function evalPlugin(plugin: BunnyPlugin) {
|
||||
const vdObject = await createVdPluginObject(plugin);
|
||||
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}?hash=${plugin.manifest.hash}`;
|
||||
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.source}?hash=${plugin.manifest.hash}`;
|
||||
|
||||
const raw = (0, eval)(pluginString)(vdObject);
|
||||
const ret = typeof raw === "function" ? raw() : raw;
|
||||
|
|
@ -99,79 +145,91 @@ export async function evalPlugin(plugin: BunnyPlugin) {
|
|||
}
|
||||
|
||||
export async function startPlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
if (!plugin) throw new Error("Attempted to start non-existent plugin");
|
||||
const plugin = getPluginById(id);
|
||||
invariant(plugin, "Attempted to start non-existent plugin");
|
||||
|
||||
try {
|
||||
if (!settings.safeMode?.enabled) {
|
||||
if (pluginsEnabled()) {
|
||||
const pluginRet: EvaledPlugin = await evalPlugin(plugin);
|
||||
loadedPlugins[id] = pluginRet;
|
||||
_pluginInstances[id] = pluginRet;
|
||||
pluginRet.onLoad?.();
|
||||
}
|
||||
|
||||
delete plugin.error;
|
||||
plugin.enabled = true;
|
||||
} catch (e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e);
|
||||
logger.error(`Plugin ${plugin.source} errored whilst loading, and will be unloaded`, e);
|
||||
plugin.error = e instanceof Error ? e.stack : String(e);
|
||||
|
||||
try {
|
||||
loadedPlugins[plugin.id]?.onUnload?.();
|
||||
_pluginInstances[id]?.onUnload?.();
|
||||
} catch (e2) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e2);
|
||||
logger.error(`Plugin ${plugin.source} errored whilst unloading`, e2);
|
||||
}
|
||||
|
||||
delete loadedPlugins[id];
|
||||
delete _pluginInstances[id];
|
||||
plugin.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function stopPlugin(id: string, disable = true) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
const pluginRet = loadedPlugins[id];
|
||||
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
||||
const plugin = getPluginById(id);
|
||||
const pluginInstance = _pluginInstances[id];
|
||||
invariant(plugin, "Attempted to stop non-existent plugin");
|
||||
|
||||
if (!settings.safeMode?.enabled) {
|
||||
if (pluginsEnabled()) {
|
||||
try {
|
||||
pluginRet?.onUnload?.();
|
||||
pluginInstance?.onUnload?.();
|
||||
} catch (e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e);
|
||||
logger.error(`Plugin ${plugin.source} errored whilst unloading`, e);
|
||||
}
|
||||
|
||||
delete loadedPlugins[id];
|
||||
delete _pluginInstances[id];
|
||||
}
|
||||
|
||||
disable && (plugin.enabled = false);
|
||||
if (disable) plugin.enabled = false;
|
||||
}
|
||||
|
||||
export async function removePlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
const plugin = getPluginById(id);
|
||||
invariant(plugin, "Removing non-existent plugin");
|
||||
if (plugin.enabled) stopPlugin(id);
|
||||
delete plugins[id];
|
||||
await purgeStorage(id);
|
||||
delete sourceToObject[plugin.source];
|
||||
await purgeStorage(plugin.source);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export async function initPlugins() {
|
||||
await awaitSyncWrapper(settings);
|
||||
await awaitSyncWrapper(plugins);
|
||||
const allIds = Object.keys(plugins);
|
||||
await awaitStorage(sourceToObject, idToSource, settings);
|
||||
|
||||
if (!settings.safeMode?.enabled) {
|
||||
// Loop over any plugin that is enabled, update it if allowed, then start it.
|
||||
await allSettled(allIds.filter(pl => plugins[pl].enabled).map(async pl => (plugins[pl].update && await fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await startPlugin(pl))));
|
||||
// Wait for the above to finish, then update all disabled plugins that are allowed to.
|
||||
allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => fetchPlugin(pl));
|
||||
// TODO: Optimize this
|
||||
if (pluginsEnabled()) {
|
||||
const allIds = [...new Set(Object.values(sourceToObject).map(s => s!.pluginId))];
|
||||
|
||||
// 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 {
|
||||
await fetchPlugin(pl);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
await startPlugin(pl);
|
||||
}));
|
||||
|
||||
// Loop over disabled plugins that are allowed to update
|
||||
allIds
|
||||
.filter(id => !getPluginById(id)?.enabled && getPluginById(id)!.update)
|
||||
.forEach(id => fetchPlugin(id));
|
||||
}
|
||||
|
||||
return stopAllPlugins;
|
||||
return () => Object.keys(_pluginInstances).forEach(p => stopPlugin(p, false));
|
||||
}
|
||||
|
||||
const stopAllPlugins = () => Object.keys(loadedPlugins).forEach(p => stopPlugin(p, false));
|
||||
|
||||
export const getSettings = (id: string) => loadedPlugins[id]?.settings;
|
||||
export function getSettingsComponent(id: string) {
|
||||
return _pluginInstances[id]?.settings;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { allSettled } from "@core/polyfills/allSettled";
|
|||
import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader";
|
||||
import { ThemeManager } from "@lib/api/native/modules";
|
||||
import { after, before, instead } from "@lib/api/patcher";
|
||||
import { awaitSyncWrapper, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
|
||||
import { findInReactTree, safeFetch } from "@lib/utils";
|
||||
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
|
@ -230,7 +230,7 @@ export function getThemeFromLoader(): Theme | null {
|
|||
}
|
||||
|
||||
export async function updateThemes() {
|
||||
await awaitSyncWrapper(themes);
|
||||
await awaitStorage(themes);
|
||||
const currentTheme = getThemeFromLoader();
|
||||
await allSettled(Object.keys(themes).map(id => fetchTheme(id, currentTheme?.id === id)));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
export const DISCORD_SERVER = "https://discord.gg/XjYgWXHb9Q";
|
||||
export const GITHUB = "https://github.com/pyoncord";
|
||||
export const PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
||||
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
|
||||
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
|
||||
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
|
||||
export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
|
||||
|
||||
|
|
|
|||
23
src/lib/utils/invariant.ts
Normal file
23
src/lib/utils/invariant.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* `invariant` is used to [assert](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) that the `condition` is [truthy](https://github.com/getify/You-Dont-Know-JS/blob/bdbe570600d4e1107d0b131787903ca1c9ec8140/up%20%26%20going/ch2.md#truthy--falsy).
|
||||
* 💥 `invariant` will `throw` an `Error` if the `condition` is [falsey](https://github.com/getify/You-Dont-Know-JS/blob/bdbe570600d4e1107d0b131787903ca1c9ec8140/up%20%26%20going/ch2.md#truthy--falsy)
|
||||
* 🤏 `message`s are not displayed in production environments to help keep bundles small
|
||||
*
|
||||
* ```ts
|
||||
* const value: Person | null = { name: "Alex" };
|
||||
* invariant(value, "Expected value to be a person");
|
||||
* // type of `value`` has been narrowed to `Person`
|
||||
* ```
|
||||
*/
|
||||
export default function invariant(
|
||||
condition: any,
|
||||
message?: string | (() => string),
|
||||
): asserts condition {
|
||||
if (condition) return;
|
||||
|
||||
const resolvedMessage: string | undefined = typeof message === "function" ? message() : message;
|
||||
const prefix = "[Invariant Violation]";
|
||||
const value = resolvedMessage ? `${prefix}: ${resolvedMessage}` : prefix;
|
||||
|
||||
throw new Error(value);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue