import * as alerts from "@core/vendetta/alerts"; import * as storage from "@core/vendetta/storage"; import { createStorage } from "@core/vendetta/storage"; import * as themes from "@lib/addons/themes"; import * as assets from "@lib/api/assets"; import * as commands from "@lib/api/commands"; import * as debug from "@lib/api/debug"; import { getVendettaLoaderIdentity, isPyonLoader, } from "@lib/api/native/loader"; import patcher from "@lib/api/patcher"; import { loaderConfig, settings } from "@lib/api/settings"; import * as utils from "@lib/utils"; import { cyrb64Hash } from "@lib/utils/cyrb64"; import { LoggerClass } from "@lib/utils/logger"; import * as metro from "@metro"; import * as common from "@metro/common"; import { Forms } from "@metro/common/components"; import * as commonComponents from "@metro/common/components"; 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"; import { VdPluginManager, VendettaPlugin } from "./plugins"; export async function createVdPluginObject(plugin: VendettaPlugin) { return { ...window.vendetta, plugin: { id: plugin.id, manifest: plugin.manifest, // Wrapping this with wrapSync is NOT an option. storage: await createStorage>( storage.createMMKVBackend(plugin.id), ), }, logger: new LoggerClass(`ShiggyCord ยป ${plugin.manifest.name}`), }; } export const initVendettaObject = (): any => { // pitfall: this assumes the returning module(s) are the same within the same location // find(m => m.render?.name === "ActionSheet") - would work fine // ["trackThis", "trackThat"].forEach(p => find(m => m[p])) - would not const createStackBasedFilter = (fn: any) => { return (filter: (m: any) => boolean) => { return fn( metro.factories.createSimpleFilter( filter, cyrb64Hash(new Error().stack!), ), ); }; }; const api = (window.vendetta = { patcher: { before: patcher.before, after: patcher.after, instead: patcher.instead, }, metro: { modules: window.modules, find: createStackBasedFilter(metro.findExports), findAll: createStackBasedFilter(metro.findAllExports), findByProps: (...props: any[]) => { // TODO: remove this hack to fix Decor if (props.length === 1 && props[0] === "KeyboardAwareScrollView") { props.push("listenToKeyboardEvents"); } const ret = metro.findByProps(...props); if (ret == null) { if (props.includes("ActionSheetTitleHeader")) { const module = metro.findByProps("ActionSheetRow"); // returning a fake object probably wouldn't cause an issue, // since the original object are full of getters anyway return { ...module, ActionSheetTitleHeader: module.BottomSheetTitleHeader, ActionSheetContentContainer: ({ children }: any) => { useEffect( () => console.warn( "Discord has removed 'ActionSheetContentContainer', please move into something else. This has been temporarily replaced with View", ), [], ); return createElement(View, null, children); }, }; } } return ret; }, findByPropsAll: (...props: any) => metro.findByPropsAll(...props), findByName: (name: string, defaultExp?: boolean | undefined) => { // TODO: remove this hack to fix Decor if (name === "create" && typeof defaultExp === "undefined") { return metro.findByName("create", false).default; } return metro.findByName(name, defaultExp ?? true); }, findByNameAll: (name: string, defaultExp: boolean = true) => metro.findByNameAll(name, defaultExp), findByDisplayName: (displayName: string, defaultExp: boolean = true) => metro.findByDisplayName(displayName, defaultExp), findByDisplayNameAll: (displayName: string, defaultExp: boolean = true) => metro.findByDisplayNameAll(displayName, defaultExp), findByTypeName: (typeName: string, defaultExp: boolean = true) => metro.findByTypeName(typeName, defaultExp), findByTypeNameAll: (typeName: string, defaultExp: boolean = true) => metro.findByTypeNameAll(typeName, defaultExp), findByStoreName: (name: string) => metro.findByStoreName(name), common: { constants: common.constants, channels: common.channels, i18n: common.i18n, url: common.url, toasts: common.toasts, stylesheet: { createThemedStyleSheet, }, clipboard: common.clipboard, assets: common.assets, invites: common.invites, commands: common.commands, navigation: common.navigation, navigationStack: common.navigationStack, NavigationNative: common.NavigationNative, Flux: common.Flux, FluxDispatcher: common.FluxDispatcher, React: common.React, ReactNative: common.ReactNative, get moment() { // Lazy-require heavy libs to avoid pulling them into the startup bundle. // Consumers access `window.vendetta.common.moment` and this getter will // require the module on first access. return require("moment"); }, get chroma() { // Lazy-load chroma-js to reduce startup cost const cached = (this as any)._chroma; if (cached) return cached; const loaded = require("chroma-js"); (this as any)._chroma = loaded; return loaded; }, get lodash() { return require("lodash"); }, get util() { return require("util"); }, }, }, constants: { DISCORD_SERVER: "https://discord.gg/n9QQ4XhhJP", GITHUB: "https://github.com/vendetta-mod", PROXY_PREFIX: "https://vd-plugins.github.io/proxy", HTTP_REGEX: /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/, HTTP_REGEX_MULTI: /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g, DISCORD_SERVER_ID: "1015931589865246730", PLUGINS_CHANNEL_ID: "1091880384561684561", THEMES_CHANNEL_ID: "1091880434939482202", }, utils: { findInReactTree: (tree: { [key: string]: any }, filter: any) => utils.findInReactTree(tree, filter), 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) => omit(object, keys), }, debug: { connectToDebugger: (url: string) => debug.connectToDebugger(url), getDebugInfo: () => debug.getDebugInfo(), }, ui: { components: { Forms, General: common.ReactNative, Alert: commonComponents.LegacyAlert, Button: commonComponents.CompatButton, HelpMessage: (...props: any[]) => ( ), SafeAreaView: (...props: any[]) => ( ), Summary: components.Summary, ErrorBoundary: components.ErrorBoundary, Codeblock: components.Codeblock, Search: components.Search, }, toasts: { showToast: (content: string, asset?: number) => toasts.showToast(content, asset), }, alerts: { showConfirmationAlert: (options: any) => alerts.showConfirmationAlert(options), showCustomAlert: (component: React.ComponentType, props: any) => alerts.showCustomAlert(component, props), showInputAlert: (options: any) => alerts.showInputAlert(options), }, assets: { all: new Proxy( {}, { get(cache, p) { if (typeof p !== "string") return undefined; if (cache[p]) return cache[p]; for (const asset of assets.iterateAssets()) { if (asset.name) return (cache[p] = asset); } }, ownKeys(cache) { const keys = new Set(); for (const asset of assets.iterateAssets()) { cache[asset.name] = asset; keys.add(asset.name); } return [...keys]; }, }, ), find: (filter: (a: any) => boolean) => assets.findAsset(filter), getAssetByName: (name: string) => assets.findAsset(name), getAssetByID: (id: number) => assets.findAsset(id), getAssetIDByName: (name: string) => assets.findAssetId(name), }, semanticColors: color.semanticColors, rawColors: color.rawColors, }, plugins: { 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, fetchTheme: (id: string, selected?: boolean) => themes.fetchTheme(id, selected), installTheme: (id: string) => themes.installTheme(id), selectTheme: (id: string) => themes.selectTheme(id === "default" ? null : themes.themes[id]), removeTheme: (id: string) => themes.removeTheme(id), getCurrentTheme: () => themes.getThemeFromLoader(), updateThemes: () => themes.updateThemes(), }, commands: { registerCommand: commands.registerCommand, }, storage: { createProxy: (target: any) => storage.createProxy(target), useProxy: (_storage: any) => storage.useProxy(_storage), createStorage: (backend: any) => storage.createStorage(backend), wrapSync: (store: any) => storage.wrapSync(store), awaitSyncWrapper: (store: any) => storage.awaitStorage(store), createMMKVBackend: (store: string) => storage.createMMKVBackend(store), createFileBackend: (file: string) => { // Redirect path to vendetta_theme.json if (isPyonLoader() && file === "vendetta_theme.json") { file = "pyon/current-theme.json"; } return storage.createFileBackend(file); }, }, settings, loader: { identity: getVendettaLoaderIdentity() ?? void 0, config: loaderConfig, }, logger: { log: (...message: any) => console.log(...message), info: (...message: any) => console.info(...message), warn: (...message: any) => console.warn(...message), error: (...message: any) => console.error(...message), time: (...message: any) => console.time(...message), trace: (...message: any) => console.trace(...message), verbose: (...message: any) => console.log(...message), }, version: debug.versionHash, unload: () => { delete window.vendetta; }, }); return () => api.unload(); };