chore: separate vd plugins from bunny
This commit is contained in:
parent
06a852639f
commit
a8d2ddd0df
12 changed files with 279 additions and 98 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types";
|
||||
import { BunnyPlugin, sourceStore as pluginStorage } from "@lib/managers/plugins";
|
||||
import { messageUtil } from "@metro/common";
|
||||
|
||||
export default () => <ApplicationCommand>{
|
||||
|
|
@ -15,7 +15,7 @@ export default () => <ApplicationCommand>{
|
|||
}
|
||||
],
|
||||
execute([ephemeral], ctx) {
|
||||
const plugins = Object.values(pluginStorage).filter(Boolean) as unknown as BunnyPlugin[];
|
||||
const plugins = Object.values(VdPluginManager.plugins).filter(Boolean) as unknown as VendettaPlugin[];
|
||||
plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name));
|
||||
|
||||
const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { findAssetId } 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, removePlugin,sourceStore } from "@lib/managers/plugins";
|
||||
import { installTheme, removeTheme, themes } from "@lib/managers/themes";
|
||||
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";
|
||||
|
|
@ -21,11 +21,11 @@ const forumReactions = findByPropsLazy("MostCommonForumPostReaction");
|
|||
|
||||
const postMap = {
|
||||
Plugin: {
|
||||
storage: sourceStore,
|
||||
storage: VdPluginManager.plugins,
|
||||
urlsFilter: (url: string) => url.startsWith(VD_PROXY_PREFIX),
|
||||
installOrRemove: (url: string) => {
|
||||
const isInstalled = postMap.Plugin.storage[url];
|
||||
return isInstalled ? removePlugin(url) : installPlugin(url);
|
||||
return isInstalled ? VdPluginManager.removePlugin(url) : VdPluginManager.installPlugin(url);
|
||||
}
|
||||
},
|
||||
Theme: {
|
||||
|
|
@ -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(sourceStore);
|
||||
useProxy(VdPluginManager.plugins);
|
||||
useProxy(themes);
|
||||
|
||||
const [isInstalling, setIsInstalling] = React.useState(false);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
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 { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
|
|
@ -28,7 +28,7 @@ function typeFromUrl(url: string) {
|
|||
}
|
||||
|
||||
function installWithToast(type: "plugin" | "theme", url: string) {
|
||||
(type === "plugin" ? installPlugin : installTheme)(url)
|
||||
(type === "plugin" ? VdPluginManager.installPlugin.bind(VdPluginManager) : installTheme)(url)
|
||||
.then(() => {
|
||||
showToast(Strings.SUCCESSFULLY_INSTALLED, findAssetId("Check"));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { CardWrapper } from "@core/ui/components/AddonCard";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
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";
|
||||
import { NavigationNative, tokens } from "@metro/common";
|
||||
import { NavigationNative } from "@metro/common";
|
||||
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
|
||||
import { createContext, memo, useContext } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
|
|
@ -13,7 +13,7 @@ import { Image, View } from "react-native";
|
|||
import { usePluginCardStyles } from "./usePluginCardStyles";
|
||||
|
||||
const { useToken } = lazyDestructure(() => findByProps("useToken"));
|
||||
const PluginContext = createContext<BunnyPlugin>(null!);
|
||||
const PluginContext = createContext<VendettaPlugin>(null!);
|
||||
const usePlugin = () => useContext(PluginContext);
|
||||
|
||||
function Authors() {
|
||||
|
|
@ -65,25 +65,25 @@ function Title() {
|
|||
|
||||
// TODO: Wrap in a Card-ish component with red bg
|
||||
// TODO: Allow glacing at the error's stack
|
||||
function Status() {
|
||||
const plugin = usePlugin();
|
||||
const styles = usePluginCardStyles();
|
||||
const INTERACTIVE_NORMAL = useToken(tokens.colors.INTERACTIVE_NORMAL);
|
||||
// function Status() {
|
||||
// const plugin = usePlugin();
|
||||
// const styles = usePluginCardStyles();
|
||||
// const INTERACTIVE_NORMAL = useToken(tokens.colors.INTERACTIVE_NORMAL);
|
||||
|
||||
if (!plugin.error) return null;
|
||||
// if (!plugin.error) return null;
|
||||
|
||||
return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
<View style={styles.smallIcon}>
|
||||
<Image
|
||||
tintColor={INTERACTIVE_NORMAL}
|
||||
source={findAssetId("WarningIcon")}
|
||||
/>
|
||||
</View>
|
||||
<Text variant="text-sm/semibold">
|
||||
There was an error while attempting to start this plugin.
|
||||
</Text>
|
||||
</View>;
|
||||
}
|
||||
// return <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
||||
// <View style={styles.smallIcon}>
|
||||
// <Image
|
||||
// tintColor={INTERACTIVE_NORMAL}
|
||||
// source={findAssetId("WarningIcon")}
|
||||
// />
|
||||
// </View>
|
||||
// <Text variant="text-sm/semibold">
|
||||
// There was an error while attempting to start this plugin.
|
||||
// </Text>
|
||||
// </View>;
|
||||
// }
|
||||
|
||||
const Actions = memo(() => {
|
||||
const plugin = usePlugin();
|
||||
|
|
@ -94,10 +94,10 @@ const Actions = memo(() => {
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
icon={findAssetId("WrenchIcon")}
|
||||
disabled={!getSettingsComponent(plugin.id)}
|
||||
disabled={!VdPluginManager.getSettings(plugin.id)}
|
||||
onPress={() => navigation.push("VendettaCustomPage", {
|
||||
title: plugin.manifest.name,
|
||||
render: getSettingsComponent(plugin.id),
|
||||
render: VdPluginManager.getSettings(plugin.id),
|
||||
})}
|
||||
/>
|
||||
<IconButton
|
||||
|
|
@ -113,14 +113,14 @@ const Actions = memo(() => {
|
|||
</View>;
|
||||
});
|
||||
|
||||
export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
|
||||
export default function PluginCard({ item: plugin }: CardWrapper<VendettaPlugin>) {
|
||||
useProxy(plugin);
|
||||
|
||||
return (
|
||||
<PluginContext.Provider value={plugin}>
|
||||
<Card>
|
||||
<Stack spacing={16}>
|
||||
<Status />
|
||||
{/* <Status /> */}
|
||||
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Title />
|
||||
|
|
@ -132,8 +132,8 @@ export default function PluginCard({ item: plugin }: CardWrapper<BunnyPlugin>) {
|
|||
<TableSwitch
|
||||
value={plugin.enabled}
|
||||
onValueChange={(v: boolean) => {
|
||||
if (v) startPlugin(plugin.id);
|
||||
else stopPlugin(plugin.id);
|
||||
if (v) VdPluginManager.startPlugin(plugin.id);
|
||||
else VdPluginManager.stopPlugin(plugin.id);
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Strings } from "@core/i18n";
|
||||
import AddonPage from "@core/ui/components/AddonPage";
|
||||
import PluginCard from "@core/ui/settings/pages/Plugins/PluginCard";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { useProxy } from "@lib/api/storage";
|
||||
import { BunnyPlugin, installPlugin, preferredSourceStore, sourceStore } from "@lib/managers/plugins";
|
||||
import { settings } from "@lib/settings";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
||||
|
|
@ -10,16 +10,15 @@ export default function Plugins() {
|
|||
useProxy(settings);
|
||||
|
||||
return (
|
||||
<AddonPage<BunnyPlugin>
|
||||
<AddonPage<VendettaPlugin>
|
||||
title={Strings.PLUGINS}
|
||||
searchKeywords={[
|
||||
"manifest.name",
|
||||
"manifest.description",
|
||||
p => p.manifest.authors?.map((a: Author) => a.name).join()
|
||||
]}
|
||||
items={preferredSourceStore}
|
||||
resolveItem={source => sourceStore[source]}
|
||||
fetchFunction={installPlugin}
|
||||
items={VdPluginManager.plugins}
|
||||
fetchFunction={VdPluginManager.installPlugin.bind(VdPluginManager)}
|
||||
safeModeMessage={Strings.SAFE_MODE_NOTICE_PLUGINS}
|
||||
card={PluginCard}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { formatString, Strings } from "@core/i18n";
|
||||
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
|
||||
import { findAssetId } from "@lib/api/assets";
|
||||
import { purgeStorage, useProxy } from "@lib/api/storage";
|
||||
import { BunnyPlugin, fetchAndStorePlugin, 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";
|
||||
|
|
@ -11,14 +11,14 @@ import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/comm
|
|||
import { ScrollView, View } from "react-native";
|
||||
|
||||
interface InfoProps {
|
||||
plugin: BunnyPlugin;
|
||||
plugin: VendettaPlugin;
|
||||
navigation: any;
|
||||
}
|
||||
|
||||
export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps) {
|
||||
useProxy(plugin);
|
||||
|
||||
const Settings = getSettingsComponent(plugin.id);
|
||||
const Settings = VdPluginManager.getSettings(plugin.id);
|
||||
|
||||
return <ActionSheet>
|
||||
<ScrollView>
|
||||
|
|
@ -47,16 +47,16 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
|
|||
label={Strings.REFETCH}
|
||||
icon={<TableRow.Icon source={findAssetId("RetryIcon")} />}
|
||||
onPress={async () => {
|
||||
if (plugin.enabled) stopPlugin(plugin.id, false);
|
||||
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await fetchAndStorePlugin(plugin.id);
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
}
|
||||
|
||||
if (plugin.enabled) await startPlugin(plugin.id);
|
||||
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}}
|
||||
/>
|
||||
|
|
@ -89,10 +89,10 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
|
|||
cancelText: Strings.CANCEL,
|
||||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: async () => {
|
||||
if (plugin.enabled) stopPlugin(plugin.id, false);
|
||||
if (plugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
|
||||
|
||||
try {
|
||||
await fetchAndStorePlugin(plugin.id);
|
||||
await VdPluginManager.fetchPlugin(plugin.id);
|
||||
showToast(Strings.PLUGIN_REFETCH_SUCCESSFUL, findAssetId("toast_image_saved"));
|
||||
} catch {
|
||||
showToast(Strings.PLUGIN_REFETCH_FAILED, findAssetId("Small"));
|
||||
|
|
@ -111,7 +111,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
|
|||
findAssetId(message[1])
|
||||
);
|
||||
|
||||
if (plugin.enabled) await startPlugin(plugin.id);
|
||||
if (plugin.enabled) await VdPluginManager.startPlugin(plugin.id);
|
||||
hideSheet("PluginInfoActionSheet");
|
||||
}
|
||||
})}
|
||||
|
|
@ -127,7 +127,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: InfoProps)
|
|||
confirmColor: ButtonColors.RED,
|
||||
onConfirm: () => {
|
||||
try {
|
||||
removePlugin(plugin.id);
|
||||
VdPluginManager.removePlugin(plugin.id);
|
||||
} catch (e) {
|
||||
showToast(String(e), findAssetId("Small"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import patcher from "@lib/api/patcher";
|
|||
import * as storage from "@lib/api/storage";
|
||||
import { createStorage } from "@lib/api/storage";
|
||||
import * as debug from "@lib/debug";
|
||||
import * as plugins from "@lib/managers/plugins";
|
||||
import * as themes from "@lib/managers/themes";
|
||||
import { loaderConfig, settings } from "@lib/settings";
|
||||
import * as utils from "@lib/utils";
|
||||
import { cyrb64Hash } from "@lib/utils/cyrb64";
|
||||
import { DiscordLogger } from "@lib/utils/logger";
|
||||
import * as metro from "@metro";
|
||||
import * as common from "@metro/common";
|
||||
|
|
@ -23,7 +23,9 @@ import { omit } from "es-toolkit";
|
|||
import { createElement, useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
export async function createVdPluginObject(plugin: plugins.BunnyPlugin) {
|
||||
import { VdPluginManager, VendettaPlugin } from "./plugins";
|
||||
|
||||
export async function createVdPluginObject(plugin: VendettaPlugin) {
|
||||
return {
|
||||
...window.vendetta,
|
||||
plugin: {
|
||||
|
|
@ -171,13 +173,13 @@ export const initVendettaObject = (): any => {
|
|||
rawColors: color.rawColors
|
||||
},
|
||||
plugins: {
|
||||
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]!.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)
|
||||
plugins: VdPluginManager.plugins,
|
||||
fetchPlugin: (source: string) => VdPluginManager.fetchPlugin(source),
|
||||
installPlugin: (source: string, enabled = true) => VdPluginManager.installPlugin(source, enabled),
|
||||
startPlugin: (id: string) => VdPluginManager.startPlugin(id),
|
||||
stopPlugin: (id: string, disable = true) => VdPluginManager.stopPlugin(id, disable),
|
||||
removePlugin: (id: string) => VdPluginManager.removePlugin(id),
|
||||
getSettings: (id: string) => VdPluginManager.getSettings(id)
|
||||
},
|
||||
themes: {
|
||||
themes: themes.themes,
|
||||
|
|
@ -229,32 +231,3 @@ export const initVendettaObject = (): any => {
|
|||
|
||||
return () => api.unload();
|
||||
};
|
||||
|
||||
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
|
||||
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
|
||||
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
|
||||
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
const cyrb64 = (str: string, seed = 0) => {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
// For a single 53-bit numeric return value we could return
|
||||
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
// but we instead return the full 64-bit value:
|
||||
return [h2 >>> 0, h1 >>> 0];
|
||||
};
|
||||
|
||||
// An improved, *insecure* 64-bit hash that's short, fast, and has no dependencies.
|
||||
// Output is always 14 characters.
|
||||
const cyrb64Hash = (str: string, seed = 0) => {
|
||||
const [h2, h1] = cyrb64(str, seed);
|
||||
return h2.toString(36).padStart(7, "0") + h1.toString(36).padStart(7, "0");
|
||||
};
|
||||
184
src/core/vendetta/plugins.ts
Normal file
184
src/core/vendetta/plugins.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import { allSettled } from "@core/polyfills/allSettled";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { settings } from "@lib/settings";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
|
||||
import { DiscordLogger, logger } from "@lib/utils/logger";
|
||||
import { Author } from "@lib/utils/types";
|
||||
|
||||
type EvaledPlugin = {
|
||||
onLoad?(): void;
|
||||
onUnload(): void;
|
||||
settings: JSX.Element;
|
||||
};
|
||||
|
||||
// See https://github.com/vendetta-mod/polymanifest
|
||||
interface PluginManifest {
|
||||
name: string;
|
||||
description: string;
|
||||
authors: Author[];
|
||||
main: string;
|
||||
hash: string;
|
||||
// Vendor-specific field, contains our own data
|
||||
vendetta?: {
|
||||
icon?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VendettaPlugin {
|
||||
id: string;
|
||||
manifest: PluginManifest;
|
||||
enabled: boolean;
|
||||
update: boolean;
|
||||
js: string;
|
||||
}
|
||||
|
||||
const plugins = wrapSync(createStorage<Record<string, VendettaPlugin>>(createMMKVBackend("VENDETTA_PLUGINS")));
|
||||
const pluginInstance: Record<string, EvaledPlugin> = {};
|
||||
|
||||
export const VdPluginManager = {
|
||||
plugins,
|
||||
async pluginFetch(url: string) {
|
||||
if (url.startsWith(VD_PROXY_PREFIX)) {
|
||||
url = url.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
|
||||
}
|
||||
|
||||
return await safeFetch(url, { cache: "no-store" });
|
||||
},
|
||||
|
||||
async fetchPlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const existingPlugin = plugins[id];
|
||||
|
||||
let pluginManifest: PluginManifest;
|
||||
|
||||
try {
|
||||
pluginManifest = await (await this.pluginFetch(id + "manifest.json")).json();
|
||||
} catch {
|
||||
throw new Error(`Failed to fetch manifest for ${id}`);
|
||||
}
|
||||
|
||||
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 this.pluginFetch(id + (pluginManifest.main || "index.js"))).text();
|
||||
} catch { } // Empty catch, checked below
|
||||
}
|
||||
|
||||
if (!pluginJs && !existingPlugin) throw new Error(`Failed to fetch JS for ${id}`);
|
||||
|
||||
plugins[id] = {
|
||||
id: id,
|
||||
manifest: pluginManifest,
|
||||
enabled: existingPlugin?.enabled ?? false,
|
||||
update: existingPlugin?.update ?? true,
|
||||
js: pluginJs ?? existingPlugin.js,
|
||||
};
|
||||
},
|
||||
|
||||
async installPlugin(id: string, enabled = true) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
if (typeof id !== "string" || id in plugins) throw new Error("Plugin already installed");
|
||||
await this.fetchPlugin(id);
|
||||
if (enabled) await this.startPlugin(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async evalPlugin(plugin: VendettaPlugin) {
|
||||
const vendettaForPlugins = {
|
||||
...window.vendetta,
|
||||
plugin: {
|
||||
id: plugin.id,
|
||||
manifest: plugin.manifest,
|
||||
// Wrapping this with wrapSync is NOT an option.
|
||||
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
|
||||
},
|
||||
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
|
||||
};
|
||||
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;
|
||||
|
||||
const raw = (0, eval)(pluginString)(vendettaForPlugins);
|
||||
const ret = typeof raw === "function" ? raw() : raw;
|
||||
return ret?.default ?? ret ?? {};
|
||||
},
|
||||
|
||||
async startPlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
if (!plugin) throw new Error("Attempted to start non-existent plugin");
|
||||
|
||||
try {
|
||||
if (!settings.safeMode?.enabled) {
|
||||
const pluginRet: EvaledPlugin = await this.evalPlugin(plugin);
|
||||
pluginInstance[id] = pluginRet;
|
||||
pluginRet.onLoad?.();
|
||||
}
|
||||
plugin.enabled = true;
|
||||
} catch (e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e);
|
||||
|
||||
try {
|
||||
pluginInstance[plugin.id]?.onUnload?.();
|
||||
} catch (e2) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e2);
|
||||
}
|
||||
|
||||
delete pluginInstance[id];
|
||||
plugin.enabled = false;
|
||||
}
|
||||
},
|
||||
|
||||
stopPlugin(id: string, disable = true) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
const pluginRet = pluginInstance[id];
|
||||
if (!plugin) throw new Error("Attempted to stop non-existent plugin");
|
||||
|
||||
if (!settings.safeMode?.enabled) {
|
||||
try {
|
||||
pluginRet?.onUnload?.();
|
||||
} catch (e) {
|
||||
logger.error(`Plugin ${plugin.id} errored whilst unloading`, e);
|
||||
}
|
||||
|
||||
delete pluginInstance[id];
|
||||
}
|
||||
|
||||
disable && (plugin.enabled = false);
|
||||
},
|
||||
|
||||
async removePlugin(id: string) {
|
||||
if (!id.endsWith("/")) id += "/";
|
||||
const plugin = plugins[id];
|
||||
if (plugin.enabled) this.stopPlugin(id);
|
||||
delete plugins[id];
|
||||
await purgeStorage(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async initPlugins() {
|
||||
await awaitStorage(settings, plugins);
|
||||
const allIds = Object.keys(plugins);
|
||||
|
||||
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 this.fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await this.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 => this.fetchPlugin(pl));
|
||||
}
|
||||
|
||||
return () => this.stopAllPlugins();
|
||||
},
|
||||
|
||||
stopAllPlugins() {
|
||||
return Object.keys(pluginInstance).forEach(p => this.stopPlugin(p, false));
|
||||
},
|
||||
|
||||
getSettings: (id: string) => pluginInstance[id]?.settings
|
||||
};
|
||||
10
src/index.ts
10
src/index.ts
|
|
@ -1,8 +1,9 @@
|
|||
import initFixes from "@core/fixes";
|
||||
import { initFetchI18nStrings } from "@core/i18n";
|
||||
import { initCorePlugins } from "@core/plugins";
|
||||
import { initVendettaObject } from "@core/polyfills/vendettaObject";
|
||||
import initSettings from "@core/ui/settings";
|
||||
import { initVendettaObject } from "@core/vendetta/api";
|
||||
import { VdPluginManager } from "@core/vendetta/plugins";
|
||||
import { patchCommands } from "@lib/api/commands";
|
||||
import { injectFluxInterceptor } from "@lib/api/flux";
|
||||
import { removeFile, writeFile } from "@lib/api/native/fs";
|
||||
|
|
@ -10,7 +11,6 @@ import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
|
|||
import { FileManager } from "@lib/api/native/modules";
|
||||
import { patchLogHook } from "@lib/debug";
|
||||
import { updateFonts } from "@lib/managers/fonts";
|
||||
import { initPlugins } from "@lib/managers/plugins";
|
||||
import { initThemes, patchChatBackground } from "@lib/managers/themes";
|
||||
import { logger } from "@lib/utils/logger";
|
||||
import initSafeMode from "@ui/safeMode";
|
||||
|
|
@ -56,10 +56,10 @@ export default async () => {
|
|||
// Assign window object
|
||||
window.bunny = lib;
|
||||
|
||||
// Once done, load plugins
|
||||
initPlugins()
|
||||
// Once done, load Vendetta plugins
|
||||
VdPluginManager.initPlugins()
|
||||
.then(u => lib.unload.push(u))
|
||||
.catch(() => alert("Failed to initialize Bunny plugins"));
|
||||
.catch(() => alert("Failed to initialize Vendetta plugins"));
|
||||
|
||||
// Update the fonts
|
||||
updateFonts();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { allSettled } from "@core/polyfills/allSettled";
|
||||
import { createVdPluginObject } from "@core/polyfills/vendettaObject";
|
||||
import { createVdPluginObject } from "@core/vendetta/api";
|
||||
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
|
||||
import { settings } from "@lib/settings";
|
||||
import { safeFetch } from "@lib/utils";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { findAssetId } from "@lib/api/assets";
|
|||
import { lazyDestructure } from "@lib/utils/lazy";
|
||||
import { toasts } from "@metro/common";
|
||||
import { findByProps } from "@metro/utils";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
const { uuid4 } = lazyDestructure(() => findByProps("uuid4"));
|
||||
|
||||
|
|
@ -17,8 +16,5 @@ export const showToast = (content: string, asset?: number) => toasts.open({
|
|||
});
|
||||
|
||||
showToast.showCopyToClipboard = (message = Strings.COPIED_TO_CLIPBOARD) => {
|
||||
// On Android, only show toast for 12 and lower
|
||||
if (Platform.OS !== "android" || Platform.Version <= 32) {
|
||||
showToast(message, findAssetId("toast_copy_link"));
|
||||
}
|
||||
showToast(message, findAssetId("toast_copy_link"));
|
||||
};
|
||||
|
|
|
|||
29
src/lib/utils/cyrb64.ts
Normal file
29
src/lib/utils/cyrb64.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// cyrb53 (c) 2018 bryc (github.com/bryc). License: Public domain. Attribution appreciated.
|
||||
// A fast and simple 64-bit (or 53-bit) string hash function with decent collision resistance.
|
||||
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
|
||||
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
|
||||
export function cyrb64(str: string, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
// For a single 53-bit numeric return value we could return
|
||||
// 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
||||
// but we instead return the full 64-bit value:
|
||||
return [h2 >>> 0, h1 >>> 0];
|
||||
}
|
||||
|
||||
// An improved, *insecure* 64-bit hash that's short, fast, and has no dependencies.
|
||||
// Output is always 14 characters.
|
||||
export function cyrb64Hash(str: string, seed = 0) {
|
||||
const [h2, h1] = cyrb64(str, seed);
|
||||
return h2.toString(36).padStart(7, "0") + h1.toString(36).padStart(7, "0");
|
||||
}
|
||||
|
||||
Loading…
Add table
Reference in a new issue