diff --git a/src/core/commands/plugins.ts b/src/core/commands/plugins.ts index e7abe3f..b4ba1d8 100644 --- a/src/core/commands/plugins.ts +++ b/src/core/commands/plugins.ts @@ -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 () => { @@ -15,7 +15,7 @@ export default () => { } ], 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); diff --git a/src/core/plugins/quickInstall/forumPost.tsx b/src/core/plugins/quickInstall/forumPost.tsx index 3d02024..a40f603 100644 --- a/src/core/plugins/quickInstall/forumPost.tsx +++ b/src/core/plugins/quickInstall/forumPost.tsx @@ -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] { const [postType, url] = useExtractThreadContent(thread, firstMessage, actionSheet) ?? []; - useProxy(sourceStore); + useProxy(VdPluginManager.plugins); useProxy(themes); const [isInstalling, setIsInstalling] = React.useState(false); diff --git a/src/core/plugins/quickInstall/url.tsx b/src/core/plugins/quickInstall/url.tsx index 2ce980b..d7a073a 100644 --- a/src/core/plugins/quickInstall/url.tsx +++ b/src/core/plugins/quickInstall/url.tsx @@ -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")); }) diff --git a/src/core/ui/settings/pages/Plugins/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/PluginCard.tsx index 31a93f7..7d1c44d 100644 --- a/src/core/ui/settings/pages/Plugins/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/PluginCard.tsx @@ -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(null!); +const PluginContext = createContext(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 - - - - - There was an error while attempting to start this plugin. - - ; -} +// return +// +// +// +// +// There was an error while attempting to start this plugin. +// +// ; +// } 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), })} /> { ; }); -export default function PluginCard({ item: plugin }: CardWrapper) { +export default function PluginCard({ item: plugin }: CardWrapper) { useProxy(plugin); return ( - + {/* */} @@ -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> diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 0ce115d..0143fc6 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -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} /> diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index de3ebe1..a28c20e 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -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")); } diff --git a/src/core/polyfills/vendettaObject.tsx b/src/core/vendetta/api.tsx similarity index 83% rename from src/core/polyfills/vendettaObject.tsx rename to src/core/vendetta/api.tsx index 4e7950e..7268473 100644 --- a/src/core/polyfills/vendettaObject.tsx +++ b/src/core/vendetta/api.tsx @@ -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"); -}; diff --git a/src/core/vendetta/plugins.ts b/src/core/vendetta/plugins.ts new file mode 100644 index 0000000..2559677 --- /dev/null +++ b/src/core/vendetta/plugins.ts @@ -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 +}; diff --git a/src/index.ts b/src/index.ts index 7366561..cbe671f 100644 --- a/src/index.ts +++ b/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(); diff --git a/src/lib/managers/plugins.ts b/src/lib/managers/plugins.ts index 1448fe1..f40387b 100644 --- a/src/lib/managers/plugins.ts +++ b/src/lib/managers/plugins.ts @@ -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"; diff --git a/src/lib/ui/toasts.ts b/src/lib/ui/toasts.ts index a0abb90..d02c395 100644 --- a/src/lib/ui/toasts.ts +++ b/src/lib/ui/toasts.ts @@ -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")); }; diff --git a/src/lib/utils/cyrb64.ts b/src/lib/utils/cyrb64.ts new file mode 100644 index 0000000..c854598 --- /dev/null +++ b/src/lib/utils/cyrb64.ts @@ -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"); +} +