chore: merge upstream/main to dev

This commit is contained in:
PalmDevs 2024-10-17 18:17:33 +07:00
commit 6205c54bb8
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
78 changed files with 7201 additions and 2839 deletions

View file

@ -32,6 +32,7 @@
"yargs-parser": "^21.1.1"
},
"dependencies": {
"@gullerya/object-observer": "^6.1.3",
"@shopify/react-native-skia": "^1.3.8",
"@tanstack/react-query": "^5.51.16",
"@types/chroma-js": "~2.4.4",

8067
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,10 @@ import { findByPropsLazy } from "@metro/wrappers";
const jsxRuntime = findByPropsLazy("jsx", "jsxs", "Fragment");
function unproxyFirstArg<T>(args: T[]) {
if (!args[0]) {
throw new Error("The first argument (Component) is falsy. Ensure that you are passing a valid component.");
}
const factory = getProxyFactory(args[0]);
if (factory) args[0] = factory();
return args;

View file

@ -0,0 +1,61 @@
import { after } from "@lib/api/patcher";
import { onJsxCreate } from "@lib/api/react/jsx";
import { findByName } from "@metro";
import { useEffect, useState } from "react";
import { defineCorePlugin } from "..";
interface BunnyBadge {
label: string;
url: string;
}
const useBadgesModule = findByName("useBadges", false);
export default defineCorePlugin({
manifest: {
id: "bunny.badges",
name: "Badges",
version: "1.0.0",
description: "Adds badges to user's profile",
authors: [{ name: "pylixonly" }]
},
start() {
const propHolder = {} as Record<string, any>;
const badgeCache = {} as Record<string, BunnyBadge[]>;
onJsxCreate("RenderedBadge", (_, ret) => {
if (ret.props.id.match(/bunny-\d+-\d+/)) {
Object.assign(ret.props, propHolder[ret.props.id]);
}
});
after("default", useBadgesModule, ([user], r) => {
const [badges, setBadges] = useState<BunnyBadge[]>(user ? badgeCache[user.userId] ??= [] : []);
useEffect(() => {
if (user) {
fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`)
.then(r => r.json())
.then(badges => setBadges(badgeCache[user.userId] = badges));
}
}, [user]);
if (user) {
badges.forEach((badges, i) => {
propHolder[`bunny-${user.userId}-${i}`] = {
source: { uri: badges.url },
id: `bunny-${i}`,
label: badges.label
};
r.push({
id: `bunny-${user.userId}-${i}`,
description: badges.label,
icon: "_",
});
});
}
});
}
});

View file

@ -1,4 +1,4 @@
import { PluginInstanceInternal } from "@lib/plugins/types";
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
interface CorePlugin {
default: PluginInstanceInternal;
@ -7,7 +7,8 @@ interface CorePlugin {
// Called from @lib/plugins
export const getCorePlugins = (): Record<string, CorePlugin> => ({
"bunny.quickinstall": require("./quickinstall")
"bunny.quickinstall": require("./quickinstall"),
"bunny.badges": require("./badges")
});
/**

View file

@ -1,11 +1,11 @@
import { Strings } from "@core/i18n";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { installTheme, removeTheme, themes } from "@lib/addons/themes";
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 { installTheme, removeTheme, themes } from "@lib/themes";
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { HTTP_REGEX_MULTI, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { Button } from "@metro/common/components";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
@ -39,13 +39,13 @@ const postMap = {
};
function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void {
if (thread.guild_id !== DISCORD_SERVER_ID) return;
if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
// Determine what type of addon this is.
let postType: PostType;
if (thread.parent_id === PLUGINS_CHANNEL_ID) {
if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
postType = "Plugin";
} else if (thread.parent_id === THEMES_CHANNEL_ID && isThemeSupported()) {
} else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
postType = "Theme";
} else return;

View file

@ -10,7 +10,7 @@ export default defineCorePlugin({
name: "QuickInstall",
version: "1.0.0",
description: "Quickly install Vendetta plugins and themes",
authors: ["pyoncord"]
authors: [{ name: "Vendetta Team" }]
},
start() {
patches = [patchForumPost(), patchUrl()];

View file

@ -1,16 +1,16 @@
import { formatString, Strings } from "@core/i18n";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { installTheme } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after, instead } from "@lib/api/patcher";
import { installTheme } from "@lib/themes";
import { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { VD_PROXY_PREFIX,VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { channels, url } from "@metro/common";
import { byMutableProp } from "@metro/filters";
import { findExports } from "@metro/finders";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet"));
@ -66,7 +66,7 @@ export default () => {
if (!urlType) return orig.apply(this, args);
// Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== THEMES_CHANNEL_ID) return orig.apply(this, args);
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID) return orig.apply(this, args);
showConfirmationAlert({
title: Strings.HOLD_UP,

View file

@ -1,7 +1,8 @@
import { CardWrapper } from "@core/ui/components/AddonCard";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
@ -14,7 +15,6 @@ import { Image, ScrollView, View } from "react-native";
const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
@ -27,19 +27,19 @@ interface AddonPageProps<T extends object, I = any> {
safeModeHint?: {
message?: string;
footer?: ReactNode;
}
};
installAction?: {
label?: string;
// Ignored when onPress is defined!
fetchFn?: (url: string) => Promise<void>;
onPress?: () => void;
}
};
CardComponent: ComponentType<CardWrapper<T>>;
ListHeaderComponent?: ComponentType<any>;
ListFooterComponent?: ComponentType<any>;
}
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void> }) {
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void>; }) {
const [value, setValue] = React.useState("");
const [error, setError] = React.useState("");
const [isFetching, setIsFetching] = React.useState(false);
@ -55,7 +55,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
return <AlertModal
title={props.label}
content="Type in the source URL you want to install from:"
content="Enter the URL of the source you want to install from:"
extraContent={
<Stack style={{ marginTop: -12 }}>
<TextInput
@ -122,7 +122,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
}, [props.items, sortFn, search]);
const onInstallPress = useCallback(() => {
if (!props.installAction) return () => {};
if (!props.installAction) return () => { };
const { label, onPress, fetchFn } = props.installAction;
if (fetchFn) {
openAlert("AddonInputAlert", <InputAlert label={label ?? "Install"} fetchFn={fetchFn} />);
@ -175,7 +175,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
})}
/>}
</View>
{props.ListHeaderComponent && <props.ListHeaderComponent />}
{props.ListHeaderComponent && !search && <props.ListHeaderComponent />}
</View>
);

View file

@ -1,9 +1,9 @@
import PyoncordIcon from "@assets/icons/revenge.png";
import { Strings } from "@core/i18n";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { isFontSupported, isThemeSupported } from "@lib/api/native/loader";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { registerSection } from "@ui/settings";
import { version } from "bunny-build-info";
@ -19,9 +19,7 @@ export default function initSettings() {
title: () => Strings.BUNNY,
icon: { uri: PyoncordIcon },
render: () => import("@core/ui/settings/pages/General"),
rawTabsConfig: {
useTrailing: () => `(${version})`
}
useTrailing: () => `(${version})`
},
{
key: "BUNNY_PLUGINS",

View file

@ -1,11 +1,11 @@
import { Strings } from "@core/i18n";
import { CheckState, useFileExists } from "@core/ui/hooks/useFS";
import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { connectToDebugger } from "@lib/api/debug";
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
import { loaderConfig, settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { lazyDestructure } from "@lib/utils/lazy";
import { NavigationNative } from "@metro/common";
import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";

View file

@ -1,16 +1,15 @@
import { Strings } from "@core/i18n";
import { CardWrapper } from "@core/ui/components/AddonCard";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts, selectFont } from "@lib/addons/fonts";
import { findAssetId } from "@lib/api/assets";
import { BundleUpdaterManager } from "@lib/api/native/modules";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, selectFont } from "@lib/fonts";
import { lazyDestructure } from "@lib/utils/lazy";
import { ButtonColors } from "@lib/utils/types";
import { findByProps } from "@metro";
import { NavigationNative, tokens } from "@metro/common";
import { Button, Card, IconButton, Stack, Text } from "@metro/common/components";
import * as Skia from "@shopify/react-native-skia";
import { showConfirmationAlert } from "@ui/alerts";
import { TextStyleSheet } from "@ui/styles";
import { useMemo } from "react";
import { View } from "react-native";
@ -104,7 +103,7 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
content: "Reload Discord to apply changes?",
confirmText: Strings.RELOAD,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: BundleUpdaterManager.reload
});
}}

View file

@ -1,8 +1,8 @@
import { formatString, Strings } from "@core/i18n";
import { createProxy, useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
import { getCurrentTheme } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { createProxy, useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/fonts";
import { getCurrentTheme } from "@lib/themes";
import { safeFetch } from "@lib/utils";
import { NavigationNative } from "@metro/common";
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";

View file

@ -1,9 +1,9 @@
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import FontEditor from "@core/ui/settings/pages/Fonts/FontEditor";
import { useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts } from "@lib/addons/fonts";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts } from "@lib/fonts";
import { NavigationNative } from "@metro/common";
import FontCard from "./FontCard";

View file

@ -1,8 +1,8 @@
import { Strings } from "@core/i18n";
import Version from "@core/ui/settings/pages/General/Version";
import { useProxy } from "@core/vendetta/storage";
import { getDebugInfo } from "@lib/api/debug";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { Stack, TableRowGroup } from "@metro/common/components";
import { Platform, ScrollView } from "react-native";

View file

@ -1,10 +1,10 @@
import { Strings } from "@core/i18n";
import { PyoncordIcon } from "@core/ui/settings";
import About from "@core/ui/settings/pages/General/About";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { getDebugInfo, toggleSafeMode } from "@lib/api/debug";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
import { NavigationNative, url } from "@metro/common";
import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";

View file

@ -1,6 +1,6 @@
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { findAssetId } from "@lib/api/assets";
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types";
import { showToast } from "@lib/ui/toasts";
import { safeFetch } from "@lib/utils";
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
@ -41,7 +41,7 @@ async function* getManifests(repoUrl: string) {
function InstallButton(props: { id: string; }) {
const [installed, setInstalled] = useState(isPluginInstalled(props.id));
const installationState = useMutation<void, Error, { install: boolean }>({
const installationState = useMutation<void, Error, { install: boolean; }>({
mutationFn: async ({ install }) => {
await (install ? installPlugin : uninstallPlugin)(props.id, true);
},
@ -63,11 +63,11 @@ function InstallButton(props: { id: string; }) {
/>;
}
function TrailingButtons(props: { id: string }) {
function TrailingButtons(props: { id: string; }) {
return <Stack spacing={8} direction="horizontal">
<IconButton
size="sm"
onPress={() => {}}
onPress={() => { }}
variant="secondary"
icon={findAssetId("CircleInformationIcon")}
/>

View file

@ -5,12 +5,12 @@ import { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, memo, useContext, useMemo } from "react";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
import { UnifiedPluginModel } from "..";
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel> }>(null!);
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!);
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
@ -92,7 +92,7 @@ function Description() {
</Text>;
}
const Actions = memo(() => {
const Actions = () => {
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
@ -118,7 +118,7 @@ const Actions = memo(() => {
)}
/>
</View>;
});
};
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();

View file

@ -2,26 +2,29 @@ import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins";
import { Author } from "@lib/addons/types";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { useObservable } from "@lib/api/storage";
import { showToast } from "@lib/ui/toasts";
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { Author } from "@lib/utils/types";
import { findByProps } from "@metro";
import { NavigationNative } from "@metro/common";
import { Button, Card, FlashList, Text } from "@metro/common/components";
import { Card, FlashList, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
import { View } from "react-native";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
export interface UnifiedPluginModel {
id: string;
name: string;
description?: string;
authors?: Array<Author | string>;
authors?: Author[];
icon?: string;
isEnabled(): boolean;
@ -35,13 +38,6 @@ export interface UnifiedPluginModel {
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
function navigateToPluginBrowser(navigation: any) {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Plugin Browser",
render: React.lazy(() => import("../PluginBrowser")),
});
}
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
}
@ -74,40 +70,54 @@ export default function Plugins() {
const navigation = NavigationNative.useNavigation();
return <PluginPage
useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
resolveItem={unifyVdPlugin}
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
return [...vdPlugins, ...bnPlugins];
}}
ListHeaderComponent={() => {
const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX));
if (!unproxiedPlugins.length) return null;
// TODO: Make this dismissable
return <Card style={{ marginVertical: 8, gap: 4 }}>
<Text variant="heading-lg/bold">Unproxied Plugins Detected</Text>
<Text variant="text-md/medium">Installed plugins from unproxied sources may execute unreviewed code in this app without your knowledge.</Text>
<View style={{ marginTop: 8, flexDirection: "row" }}>
<Button
style={{ flexShrink: 1 }}
size="sm"
text="Review"
variant="secondary"
onPress={() => {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return <FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
renderItem={({ item: p }: any) => <Card>
<Text variant="heading-md/semibold">{p.id}</Text>
</Card>}
/>;
}
});
}}
/>
</View>
</Card>;
return <View style={{ marginVertical: 12, marginHorizontal: 10 }}>
<Card border="strong">
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}>
<View style={{ gap: 6, flexShrink: 1 }}>
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
<Text variant="text-sm/medium" color="text-muted">
Plugins installed from unproxied sources may run unverified code in this app without your awareness.
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
style={{ marginLeft: 8 }}
onPress={() => {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return <FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
renderItem={({ item: p }: any) => <Card>
<Text variant="heading-md/semibold">{p.id}</Text>
</Card>}
/>;
}
});
}}
/>
</View>
</View>
</Card>
</View>;
}}
installAction={{
label: "Install a plugin",
@ -137,61 +147,4 @@ export default function Plugins() {
}
}}
/>;
// const navigation = NavigationNative.useNavigation();
// const { width: pageWidth } = useWindowDimensions();
// const state = useSegmentedControlState({
// pageWidth,
// items: [
// {
// label: "Vendetta",
// id: "vendetta-plugins",
// page: (
// <PluginPage
// useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
// resolveItem={unifyVdPlugin}
// fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
// />
// )
// },
// {
// label: "Bunny",
// id: "bunny-plugins",
// page: (
// <PluginPage
// useItems={() => (useNewProxy(pluginSettings), [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id)))}
// resolveItem={unifyBunnyPlugin}
// ListHeaderComponent={() => (
// <View style={{ marginBottom: 10 }}>
// <HelpMessage messageType={0}>
// Bunny plugin system is in no way ready, try not getting yourself burnt ⚠️
// </HelpMessage>
// </View>
// )}
// ListFooterComponent={() => (
// <View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}>
// <Text variant="heading-lg/bold">{"Looking for more?"}</Text>
// <Button
// size="lg"
// text="Browse plugins"
// icon={findAssetId("discover")}
// onPress={() => navigateToPluginBrowser(navigation)}
// />
// </View>
// )}
// />
// )
// },
// ]
// });
// return (
// <View style={{ alignItems: "center", justifyContent: "center", height: "100%" }}>
// <View style={{ padding: 8, paddingBottom: 0 }}>
// <SegmentedControl state={state} />
// </View>
// <SegmentedControlPages state={state} />
// </View>
// );
}

View file

@ -1,6 +1,6 @@
import { useProxy } from "@lib/api/storage/new";
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent,isPluginEnabled, pluginSettings } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types";
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { useObservable } from "@lib/api/storage";
import { UnifiedPluginModel } from "..";
@ -13,7 +13,7 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
isEnabled: () => isPluginEnabled(getId(manifest)),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useProxy(pluginSettings);
useObservable([pluginSettings]);
},
toggle(start: boolean) {
start

View file

@ -1,5 +1,5 @@
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { useProxy } from "@lib/api/storage";
import { useProxy } from "@core/vendetta/storage";
import { UnifiedPluginModel } from "..";

View file

@ -9,11 +9,16 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
plugin.usePluginState();
return <ActionSheet>
<ScrollView>
<ScrollView contentContainerStyle={{ gap: 8, marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
<View style={{ gap: 4 }}>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
<Text variant="text-md/medium" color="text-muted">
{plugin.description}
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
{plugin.getPluginSettingsComponent() && <Button
size="md"
@ -30,6 +35,11 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
/>}
</View>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignContent: "center" }}>
<Text variant="text-lg/medium">
Oops, you shouldn't see this!
</Text>
</View>
</ScrollView>
</ActionSheet>;
}

View file

@ -1,11 +1,10 @@
import { formatString, Strings } from "@core/i18n";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { purgeStorage } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { purgeStorage } from "@lib/api/storage";
import { ButtonColors } from "@lib/utils/types";
import { clipboard } from "@metro/common";
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
import { showConfirmationAlert } from "@ui/alerts";
import { hideSheet } from "@ui/sheets";
import { showToast } from "@ui/toasts";
import { ScrollView, View } from "react-native";
@ -85,7 +84,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.name }),
confirmText: Strings.CLEAR,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: async () => {
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
@ -122,7 +121,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.name }),
confirmText: Strings.DELETE,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: () => {
try {
VdPluginManager.removePlugin(plugin.id);

View file

@ -1,12 +1,11 @@
import { formatString, Strings } from "@core/i18n";
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { useProxy } from "@core/vendetta/storage";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/themes";
import { ButtonColors } from "@lib/utils/types";
import { clipboard } from "@metro/common";
import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
function selectAndApply(value: boolean, theme: Theme) {
@ -68,7 +67,7 @@ export default function ThemeCard({ item: theme }: CardWrapper<Theme>) {
content: formatString("ARE_YOU_SURE_TO_DELETE_THEME", { name: theme.data.name }),
confirmText: Strings.DELETE,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: () => {
removeTheme(theme.id).then(wasSelected => {
setRemoved(true);

View file

@ -1,10 +1,10 @@
import { formatString, Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
import { useProxy } from "@core/vendetta/storage";
import { installTheme, Theme, themes } from "@lib/addons/themes";
import { Author } from "@lib/addons/types";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { installTheme, Theme, themes } from "@lib/themes";
import { Author } from "@lib/utils/types";
import { Button } from "@metro/common/components";
export default function Themes() {

View file

@ -8,8 +8,7 @@ export type EmitterEvent = "SET" | "GET" | "DEL";
export interface EmitterListenerData {
path: string[];
value: any;
parent: any;
value?: any;
}
export type EmitterListener = (
@ -17,7 +16,7 @@ export type EmitterListener = (
data: EmitterListenerData | any
) => any;
export type EmitterListeners = Record<string, Set<EmitterListener>>
export type EmitterListeners = Record<string, Set<EmitterListener>>;
export class Emitter {
listeners = Object.values(Events).reduce<EmitterListeners>(

View file

@ -1,4 +1,3 @@
import { ButtonColors } from "@lib/utils/types";
import { findByPropsLazy } from "@metro/wrappers";
import InputAlert, { InputAlertProps } from "@ui/components/InputAlert";
@ -13,7 +12,7 @@ export interface ConfirmationAlertOptions {
title?: string;
content: string | JSX.Element | (string | JSX.Element)[];
confirmText?: string;
confirmColor?: ButtonColors;
confirmColor?: string;
onConfirm: () => void;
secondaryConfirmText?: string;
onConfirmSecondary?: () => void;

View file

@ -1,20 +1,20 @@
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 storage from "@lib/api/storage";
import { createStorage } from "@lib/api/storage";
import * as themes from "@lib/themes";
import * as utils from "@lib/utils";
import { cyrb64Hash } from "@lib/utils/cyrb64";
import { DiscordLogger } from "@lib/utils/logger";
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 alerts from "@ui/alerts";
import * as color from "@ui/color";
import * as components from "@ui/components";
import { createThemedStyleSheet } from "@ui/styles";
@ -34,7 +34,7 @@ export async function createVdPluginObject(plugin: VendettaPlugin) {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(storage.createMMKVBackend(plugin.id)),
},
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
};
}

View file

@ -1,9 +1,9 @@
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { settings } from "@lib/api/settings";
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
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";
import { logger,LoggerClass } from "@lib/utils/logger";
type EvaledPlugin = {
onLoad?(): void;
@ -39,7 +39,9 @@ export const VdPluginManager = {
plugins,
async pluginFetch(url: string) {
if (url.startsWith(VD_PROXY_PREFIX)) {
url = url.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
url = url
.replace("https://bunny-mod.github.io/plugins-proxy", BUNNY_PROXY_PREFIX)
.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
}
return await safeFetch(url, { cache: "no-store" });
@ -61,7 +63,7 @@ export const VdPluginManager = {
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
try {
// by polymanifest spec, plugins should always specify their main file, but just in case
// 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
}
@ -96,7 +98,7 @@ export const VdPluginManager = {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
},
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Bunny » ${plugin.manifest.name}`),
};
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;
@ -166,7 +168,7 @@ export const VdPluginManager = {
const allIds = Object.keys(plugins);
if (!settings.safeMode?.enabled) {
// Loop over any plugin that is enabled, update it if allowed, then start it.
// Loop over any plugin that is enabled, update it if allowed, then start it.
await Promise.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));

View file

@ -0,0 +1,238 @@
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@core/vendetta/Emitter";
import { FileManager, MMKVManager } from "@lib/api/native/modules";
import { Platform } from "react-native";
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
const childrens = new WeakMap<any, any>();
const proxiedChildrenSet = new WeakSet<any>();
function createProxy(target: any, path: string[]): any {
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
const value: any = target[prop];
if (value !== undefined && value !== null) {
emitter.emit("GET", {
path: newPath,
value,
});
if (typeof value === "object") {
if (proxiedChildrenSet.has(value)) return value;
if (childrens.has(value)) return childrens.get(value);
const childrenProxy = createProxy(value, newPath);
childrens.set(value, childrenProxy);
return childrenProxy;
}
return value;
}
return value;
},
set(target, prop: string, value) {
if (typeof value === "object") {
if (childrens.has(value)) {
target[prop] = childrens.get(value);
} else {
const childrenProxy = createProxy(value, [...path, prop]);
childrens.set(value, childrenProxy);
proxiedChildrenSet.add(value);
target[prop] = childrenProxy;
}
} else {
target[prop] = value;
}
emitter.emit("SET", {
path: [...path, prop],
value: target[prop],
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
value,
path: [...path, prop],
});
return success;
},
});
}
return {
proxy: createProxy(target, []),
emitter,
};
}
export function useProxy<T>(storage: T): T {
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
if (event === "DEL" && data.value === storage) return;
forceUpdate();
};
emitter.on("SET", listener);
emitter.on("DEL", listener);
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
};
}, []);
return storage;
}
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
const data = await backend.get();
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
return proxy;
}
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
let awaited: any = undefined;
const awaitQueue: (() => void)[] = [];
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
store.then(v => {
awaited = v;
awaitQueue.forEach(cb => cb());
});
return new Proxy({} as Awaited<T>, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
// @ts-expect-error
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
),
get(target, prop, recv) {
if (prop === syncAwaitSymbol) return awaitInit;
return Reflect.get(awaited ?? target, prop, recv);
},
});
}
export function awaitStorage(...stores: any[]) {
return Promise.all(
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
);
}
export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
const filePathFixer = (file: string): string => Platform.select({
default: file,
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
});
const getMMKVPath = (name: string): string => {
if (ILLEGAL_CHARS_REGEX.test(name)) {
// Replace forbidden characters with hyphens
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
}
return `vd_mmkv/${name}`;
};
export const purgeStorage = async (store: string) => {
if (await MMKVManager.getItem(store)) {
MMKVManager.removeItem(store);
}
const mmkvPath = getMMKVPath(store);
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
await FileManager.removeFile?.("documents", mmkvPath);
}
};
export const createMMKVBackend = (store: string, defaultData = {}) => {
const mmkvPath = getMMKVPath(store);
const defaultStr = JSON.stringify(defaultData);
return createFileBackend(mmkvPath, defaultData, (async () => {
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
if (await FileManager.fileExists(path)) return;
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
// From the testing on Android, it seems to return this if the data is too large
if (oldData === "!!LARGE_VALUE!!") {
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
if (await FileManager.fileExists(cachePath)) {
oldData = await FileManager.readFile(cachePath, "utf8");
} else {
console.log(`${store}: Experienced data loss :(`);
oldData = defaultStr;
}
}
try {
JSON.parse(oldData);
} catch {
console.error(`${store} had an unparseable data while migrating`);
oldData = defaultStr;
}
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
if (await MMKVManager.getItem(store) !== null) {
MMKVManager.removeItem(store);
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
}
})());
};
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
return {
get: async () => {
await migratePromise;
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
if (await FileManager.fileExists(path)) {
const content = await FileManager.readFile(path, "utf8");
try {
return JSON.parse(content);
} catch {
// Corrupted content, ignore
}
}
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
return JSON.parse(await FileManager.readFile(path, "utf8"));
},
set: async data => {
await migratePromise;
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
}
};
};

View file

@ -3,37 +3,36 @@ import { initFetchI18nStrings } from "@core/i18n";
import initSettings from "@core/ui/settings";
import { initVendettaObject } from "@core/vendetta/api";
import { VdPluginManager } from "@core/vendetta/plugins";
import { updateFonts } from "@lib/addons/fonts";
import { initPlugins, updatePlugins } from "@lib/addons/plugins";
import { initThemes, patchChatBackground } from "@lib/addons/themes";
import { patchCommands } from "@lib/api/commands";
import { patchLogHook } from "@lib/api/debug";
import { injectFluxInterceptor } from "@lib/api/flux";
import { removeFile, writeFile } from "@lib/api/native/fs";
import { writeFile } from "@lib/api/native/fs";
import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
import { FileManager } from "@lib/api/native/modules";
import { updateFonts } from "@lib/fonts";
import { initPlugins } from "@lib/plugins";
import { initThemes, patchChatBackground } from "@lib/themes";
import { patchJsx } from "@lib/api/react/jsx";
import { logger } from "@lib/utils/logger";
import initSafeMode from "@ui/safeMode";
import { patchSettings } from "@ui/settings";
import * as lib from "./lib";
export default async () => {
// Themes
if (isThemeSupported()) {
try {
if (isPyonLoader()) {
if (FileManager.removeFile != null) {
removeFile("vendetta_theme.json", "");
} else {
writeFile("vendetta_theme.json", "null", "");
}
}
initThemes();
} catch (e) {
console.error("[Bunny] Failed to initialize themes...", e);
function maybeLoadThemes() {
if (!isThemeSupported()) return;
try {
if (isPyonLoader()) {
writeFile("../vendetta_theme.json", "null");
}
initThemes();
} catch (e) {
console.error("Failed to initialize themes", e);
}
}
export default async () => {
maybeLoadThemes();
// Load everything in parallel
await Promise.all([
@ -42,11 +41,13 @@ export default async () => {
patchLogHook(),
patchCommands(),
patchChatBackground(),
patchJsx(),
initVendettaObject(),
initFetchI18nStrings(),
initSettings(),
initFixes(),
initSafeMode()
initSafeMode(),
updatePlugins()
]).then(
// Push them all to unloader
u => u.forEach(f => f && lib.unload.push(f))
@ -60,6 +61,7 @@ export default async () => {
.then(u => lib.unload.push(u))
.catch(() => alert("Failed to initialize Vendetta plugins"));
// And then, load Bunny plugins
initPlugins();
// Update the fonts

View file

@ -1,5 +1,5 @@
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
import { safeFetch } from "@lib/utils";
type FontMap = Record<string, string>;

View file

@ -1,6 +1,6 @@
import { patcher } from "@lib/api";
import { registerCommand } from "@lib/api/commands";
import { createStorage } from "@lib/api/storage/new";
import { createStorage } from "@lib/api/storage";
import { logger } from "@lib/utils/logger";
import { registeredPlugins } from ".";
@ -39,7 +39,7 @@ export function createBunnyPluginAPI(id: string) {
},
// Added something in here? Make sure to also update BunnyPluginProperty in ./types
plugin: {
createStorage: () => createStorage(`plugins/storage/${id}.json`),
createStorage: <T extends object = any>() => createStorage<T>(`plugins/storage/${id}.json`),
manifest: registeredPlugins.get(id),
logger
}

View file

@ -1,6 +1,6 @@
import { getCorePlugins } from "@core/plugins";
import { readFile, removeFile, writeFile } from "@lib/api/native/fs";
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorageAsync } from "@lib/api/storage/new";
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorage } from "@lib/api/storage";
import { safeFetch } from "@lib/utils";
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
import { semver } from "@metro/common";
@ -51,6 +51,10 @@ function isExternalPlugin(manifest: t.BunnyPluginManifest): manifest is t.BunnyP
return "parentRepository" in manifest;
}
export function isCorePlugin(id: string) {
return corePluginInstances.has(id);
}
export function getId<T extends t.BunnyPluginManifest>(manifest: T): string {
const id = manifestToId.get(manifest);
assert(id, manifest?.name ?? "unknown", "getting ID from an unregistered/invalid manifest");
@ -93,7 +97,7 @@ export async function updateAndWritePlugin(repoUrl: string, id: string, fetchScr
await writeFile(manifest.jsPath, js);
}
await updateStorageAsync(`plugins/manifests/${id}.json`, manifest);
await updateStorage(`plugins/manifests/${id}.json`, manifest);
if (registeredPlugins.has(id)) {
const existingManifest = registeredPlugins.get(id)!;
@ -347,9 +351,24 @@ export function stopPlugin(id: string) {
pluginInstances.delete(id);
}
export async function checkAndRegisterUpdates() {
async function updateAllRepository() {
try {
await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
} catch (error) {
console.error("Failed to update official plugins repository", error);
}
await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
await updateRepository(repo);
}
}));
}
export async function updatePlugins() {
await awaitStorage(pluginRepositories, pluginSettings);
// Register core plugins
const corePlugins = getCorePlugins();
for (const id in corePlugins) {
const {
@ -367,12 +386,7 @@ export async function checkAndRegisterUpdates() {
corePluginInstances.set(id, instance);
}
// await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
// await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
// if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
// await updateRepository(repo);
// }
// }));
await updateAllRepository();
}
/**

View file

@ -1,6 +1,6 @@
import { createStorage } from "@lib/api/storage";
import { createStorage } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { Logger } from "@lib/utils/logger";
import { Author } from "@lib/utils/types";
export interface PluginRepo {
[id: string]: {
@ -26,7 +26,7 @@ export interface BunnyPluginManifest {
readonly name: string;
readonly description: string;
readonly version: string;
readonly authors: (Author | string)[];
readonly authors: Author[];
}
export interface BunnyPluginManifestInternal extends BunnyPluginManifest {

View file

@ -2,13 +2,13 @@
* Theming system in Bunny is currently a prototype, expect an unreadable theme implementation below
*/
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader";
import { ThemeManager } from "@lib/api/native/modules";
import { after, before, instead } from "@lib/api/patcher";
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";
import { byMutableProp } from "@metro/filters";
import { createLazyModule } from "@metro/lazy";
import { findByNameLazy, findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro/wrappers";

4
src/lib/addons/types.ts Normal file
View file

@ -0,0 +1,4 @@
export interface Author {
name: string;
id: string;
}

View file

@ -4,6 +4,7 @@ import { getImportingModuleId, requireModule } from "@metro/internals/modules";
// TODO: Deprecate this map, make another that maps to an array of assets (Asset[]) instead
/**
* @internal
* Pitfall: Multiple assets may have the same name, this is fine if we require the asset only for display,\
* but not when used to get the registered id/index. In some condition, this would break some plugins like HideGiftButton that gets id by name for patching.\
*/

View file

@ -1,14 +1,13 @@
import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { getLoaderName, getLoaderVersion, isThemeSupported } from "@lib/api/native/loader";
import { BundleUpdaterManager, ClientInfoManager, DeviceManager } from "@lib/api/native/modules";
import { after } from "@lib/api/patcher";
import { settings } from "@lib/api/settings";
import { getThemeFromLoader, selectTheme, themes } from "@lib/themes";
import { logger } from "@lib/utils/logger";
import { showToast } from "@ui/toasts";
import { version } from "bunny-build-info";
import { Platform, type PlatformConstants } from "react-native";
export let socket: WebSocket;
export interface RNConstants extends PlatformConstants {
// Android
@ -44,6 +43,7 @@ export async function toggleSafeMode() {
setTimeout(BundleUpdaterManager.reload, 400);
}
let socket: WebSocket;
export function connectToDebugger(url: string) {
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close();

View file

@ -4,5 +4,6 @@ export * as debug from "./debug";
export * as flux from "./flux";
export * as native from "./native";
export * as patcher from "./patcher";
export * as react from "./react";
export * as settings from "./settings";
export * as storage from "./storage";

View file

@ -4,7 +4,7 @@ import { FileManager } from "./modules";
* Removes all files in a directory from the path given
* @param path Path to the targeted directory
*/
export async function clearFolder(path: string, prefix = "pyoncord/") {
export async function clearFolder(path: string, { prefix = "pyoncord/" } = {}) {
if (typeof FileManager.clearFolder !== "function") throw new Error("'fs.clearFolder' is not supported");
return void await FileManager.clearFolder("documents", `${prefix}${path}`);
}
@ -13,7 +13,7 @@ export async function clearFolder(path: string, prefix = "pyoncord/") {
* Remove file from given path, currently no check for any failure
* @param path Path to the file
*/
export async function removeFile(path: string, prefix = "pyoncord/") {
export async function removeFile(path: string, { prefix = "pyoncord/" } = {}) {
if (typeof FileManager.removeFile !== "function") throw new Error("'fs.removeFile' is not supported");
return void await FileManager.removeFile("documents", `${prefix}${path}`);
}
@ -31,7 +31,7 @@ export async function removeCacheFile(path: string, prefix = "pyoncord/") {
* Check if the file or directory given by the path exists
* @param path Path to the file
*/
export async function fileExists(path: string, prefix = "pyoncord/") {
export async function fileExists(path: string, { prefix = "pyoncord/" } = {}) {
return await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`);
}
@ -40,7 +40,7 @@ export async function fileExists(path: string, prefix = "pyoncord/") {
* @param path Path to the file
* @param data String data to write to the file
*/
export async function writeFile(path: string, data: string, prefix = "pyoncord/"): Promise<void> {
export async function writeFile(path: string, data: string, { prefix = "pyoncord/" } = {}): Promise<void> {
if (typeof data !== "string") throw new Error("Argument 'data' must be a string");
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "utf8");
}
@ -50,7 +50,7 @@ export async function writeFile(path: string, data: string, prefix = "pyoncord/"
* @param path Path to the file
* @param fallback Fallback data to return if the file doesn't exist, and will be written to the file
*/
export async function readFile(path: string, prefix = "pyoncord/"): Promise<string> {
export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Promise<string> {
try {
return await FileManager.readFile(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`, "utf8");
} catch (err) {
@ -58,30 +58,19 @@ export async function readFile(path: string, prefix = "pyoncord/"): Promise<stri
}
}
export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
const blob = await fetch(url).then(r => r.blob());
const dataURL: string | null = await new Promise(r => {
const reader = new FileReader();
reader.onload = () => r(reader.result as unknown as string);
reader.readAsDataURL(blob);
});
let data: string;
if (dataURL == null) {
throw new Error("Failed to convert blob to data URL");
} else {
const index = dataURL.indexOf("base64,");
if (index === -1) throw new Error("dataURL does not contain base64");
data = dataURL.slice(index + 7);
/**
* Download a file from the given URL and save it to the path given
* @param url URL to download the file from
* @param path Path to save the file to
*/
export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const data = Buffer.from(arrayBuffer).toString("base64");
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
}
// // thanks Rosie, doesnt work though
// export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
// const arrayBuffer = await fetch(url).then(r => r.arrayBuffer());
// const data = Buffer.from(arrayBuffer).toString("base64");
// return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
// }

View file

@ -1,3 +1 @@
export * as fs from "./fs";
export * as loader from "./loader";
export * as modules from "./modules";

View file

@ -59,7 +59,7 @@ function polyfillVendettaLoaderIdentity() {
const id = getStoredTheme()?.id;
if (!id) return null;
const { themes } = require("@lib/themes");
const { themes } = require("@lib/addons/themes");
return themes[id] ?? getStoredTheme() ?? null;
},
configurable: true

View file

@ -1,97 +0,0 @@
const nmp = window.nativeModuleProxy;
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
export interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
export interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
/**
* Removes a file from the path given.
* (!) On Android, this always returns false, regardless if it fails or not!
* @param storageDir Either `cache` or `documents`
* @param path Path to the file to be removed
*/
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
/**
* Clear the folder from the path given
* (!) On Android, this only clears all *files* and not subdirectories!
* @param storageDir Either `cache` or `documents`
* @param path Path to the folder to be cleared
* @returns Whether the clearance succeeded
*/
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
export const MMKVManager = nmp.MMKVManager as MMKVManager;
//! 173.10 renamed this to RTNFileManager.
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as FileManager;
//! 173.13 renamed this to RTNClientInfoManager.
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
//! 173.14 renamed this to RTNDeviceManager.
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
export const { BundleUpdaterManager } = nmp;
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;

View file

@ -0,0 +1,11 @@
import { RNModules } from "./types";
const nmp = window.nativeModuleProxy;
export const MMKVManager = nmp.MMKVManager as RNModules.MMKVManager;
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as RNModules.FileManager;
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
export const { BundleUpdaterManager } = nmp;
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;

View file

@ -0,0 +1,89 @@
export namespace RNModules {
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
export interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
export interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
/**
* Removes a file from the path given.
* (!) On Android, this always returns false, regardless if it fails or not!
* @param storageDir Either `cache` or `documents`
* @param path Path to the file to be removed
*/
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
/**
* Clear the folder from the path given
* (!) On Android, this only clears all *files* and not subdirectories!
* @param storageDir Either `cache` or `documents`
* @param path Path to the folder to be cleared
* @returns Whether the clearance succeeded
*/
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
}

View file

@ -0,0 +1 @@
export * as jsx from "./jsx";

43
src/lib/api/react/jsx.ts Normal file
View file

@ -0,0 +1,43 @@
import { after } from "@lib/api/patcher";
import { findByPropsLazy } from "@metro";
type Callback = (Component: any, ret: JSX.Element) => JSX.Element;
const callbacks = new Map<string, Callback[]>();
const jsxRuntime = findByPropsLazy("jsx", "jsxs");
export function onJsxCreate(Component: string, callback: Callback) {
if (!callbacks.has(Component)) callbacks.set(Component, []);
callbacks.get(Component)!.push(callback);
}
export function deleteJsxCreate(Component: string, callback: Callback) {
if (!callbacks.has(Component)) return;
const cbs = callbacks.get(Component)!;
cbs.splice(cbs.indexOf(callback), 1);
if (cbs.length === 0) callbacks.delete(Component);
}
/**
* @internal
*/
export function patchJsx() {
const callback = ([Component]: unknown[], ret: JSX.Element) => {
// The check could be more complex, but this is fine for now to avoid overhead
if (typeof Component === "function" && callbacks.has(Component.name)) {
const cbs = callbacks.get(Component.name)!;
for (const cb of cbs) {
const _ret = cb(Component, ret);
if (_ret !== undefined) ret = _ret;
}
return ret;
}
};
const patches = [
after("jsx", jsxRuntime, callback),
after("jsxs", jsxRuntime, callback)
];
return () => patches.forEach(unpatch => unpatch());
}

View file

@ -1,5 +1,5 @@
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { getLoaderConfigPath } from "@lib/api/native/loader";
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
export interface Settings {
debuggerUrl: string;

View file

@ -1,95 +0,0 @@
import { FileManager, MMKVManager } from "@lib/api/native/modules";
import { Platform } from "react-native";
export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
const filePathFixer = (file: string): string => Platform.select({
default: file,
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
});
const getMMKVPath = (name: string): string => {
if (ILLEGAL_CHARS_REGEX.test(name)) {
// Replace forbidden characters with hyphens
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
}
return `vd_mmkv/${name}`;
};
export const purgeStorage = async (store: string) => {
if (await MMKVManager.getItem(store)) {
MMKVManager.removeItem(store);
}
const mmkvPath = getMMKVPath(store);
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
await FileManager.removeFile?.("documents", mmkvPath);
}
};
export const createMMKVBackend = (store: string, defaultData = {}) => {
const mmkvPath = getMMKVPath(store);
const defaultStr = JSON.stringify(defaultData);
return createFileBackend(mmkvPath, defaultData, (async () => {
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
if (await FileManager.fileExists(path)) return;
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
// From the testing on Android, it seems to return this if the data is too large
if (oldData === "!!LARGE_VALUE!!") {
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
if (await FileManager.fileExists(cachePath)) {
oldData = await FileManager.readFile(cachePath, "utf8");
} else {
console.log(`${store}: Experienced data loss :(`);
oldData = defaultStr;
}
}
try {
JSON.parse(oldData);
} catch {
console.error(`${store} had an unparseable data while migrating`);
oldData = defaultStr;
}
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
if (await MMKVManager.getItem(store) !== null) {
MMKVManager.removeItem(store);
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
}
})());
};
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
return {
get: async () => {
await migratePromise;
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
if (await FileManager.fileExists(path)) {
const content = await FileManager.readFile(path, "utf8");
try {
return JSON.parse(content);
} catch {
// Corrupted content, ignore
}
}
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
return JSON.parse(await FileManager.readFile(path, "utf8"));
},
set: async data => {
await migratePromise;
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
}
};
};

View file

@ -1,156 +1,190 @@
import { StorageBackend } from "@lib/api/storage/backends";
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@lib/utils/Emitter";
import { Emitter } from "@core/vendetta/Emitter";
import { Observable, ObserverOptions } from "@gullerya/object-observer";
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
import { debounce } from "es-toolkit";
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
const parentTarget = target;
const _loadedStorage = {} as Record<string, any>;
const childrens = new WeakMap<any, any>();
const proxiedChildrenSet = new WeakSet<any>();
function createProxy(target: any, path: string[]): any {
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
const value: any = target[prop];
if (value !== undefined && value !== null) {
emitter.emit("GET", {
parent: parentTarget,
path: newPath,
value,
});
if (typeof value === "object") {
if (proxiedChildrenSet.has(value)) return value;
if (childrens.has(value)) return childrens.get(value);
const childrenProxy = createProxy(value, newPath);
childrens.set(value, childrenProxy);
return childrenProxy;
}
return value;
}
return value;
},
set(target, prop: string, value) {
if (typeof value === "object") {
if (childrens.has(value)) {
target[prop] = childrens.get(value);
} else {
const childrenProxy = createProxy(value, [...path, prop]);
childrens.set(value, childrenProxy);
proxiedChildrenSet.add(value);
target[prop] = childrenProxy;
}
} else {
target[prop] = value;
}
emitter.emit("SET", {
parent: parentTarget,
path: [...path, prop],
value: target[prop],
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
value,
parent: parentTarget,
path: [...path, prop],
});
return success;
},
});
}
function createFileBackend<T extends object>(filePath: string) {
const write = debounce((data: T) => {
writeFile(filePath, JSON.stringify(data));
}, 500);
return {
proxy: createProxy(target, []),
emitter,
get: async () => {
try {
return JSON.parse(await readFile(filePath)) as T;
} catch (e) {
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
}
},
set: async (data: T) => {
if (!data || typeof data !== "object") {
throw new Error("data needs to be an object");
}
write(data);
},
exists: async () => {
return await fileExists(filePath);
}
};
}
export function useProxy<T>(storage: T): T {
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
export function useObservable(observables: Observable[], opts?: ObserverOptions) {
if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error(
"An error occured while initializing the storage",
);
if (observables.some(o => !Observable.isObservable(o))) {
throw new Error("Argument passed isn't an Observable");
}
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
if (event === "DEL" && data.value === storage) return;
forceUpdate();
};
const listener = () => forceUpdate();
emitter.on("SET", listener);
emitter.on("DEL", listener);
observables.forEach(o => Observable.observe(o, listener, opts));
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
observables.forEach(o => Observable.unobserve(o, listener));
};
}, []);
return storage;
}
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
const data = await backend.get();
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
return proxy;
export async function updateStorage<T extends object = {}>(path: string, value: T): Promise<void> {
_loadedStorage[path] = value;
createFileBackend<T>(path).set(value);
}
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
let awaited: any = undefined;
export function createStorageAndCallback<T extends object = {}>(
path: string,
cb: (proxy: T) => void,
{
dflt = {} as T,
nullIfEmpty = false
} = {}
) {
let emitter: Emitter;
const awaitQueue: (() => void)[] = [];
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
const callback = (data: any) => {
const proxy = new Proxy(Observable.from(data), {
get(target, prop, receiver) {
if (prop === Symbol.for("vendetta.storage.emitter")) {
if (emitter) return emitter;
emitter = new Emitter();
store.then(v => {
awaited = v;
awaitQueue.forEach(cb => cb());
});
Observable.observe(target, changes => {
for (const change of changes) {
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
path: change.path,
value: change.value
});
}
});
return new Proxy({} as Awaited<T>, {
return emitter;
}
return Reflect.get(target, prop, receiver);
},
});
const handler = () => backend.set(proxy);
Observable.observe(proxy, handler);
cb(proxy);
};
const backend = createFileBackend<T>(path);
if (_loadedStorage[path]) {
callback(_loadedStorage[path]);
} else {
backend.exists().then(async exists => {
if (!exists) {
if (nullIfEmpty) {
callback(_loadedStorage[path] = null);
} else {
_loadedStorage[path] = dflt;
await backend.set(dflt);
callback(dflt);
}
} else {
callback(_loadedStorage[path] = await backend.get());
}
});
}
}
type StorageOptions<T extends object> = Parameters<typeof createStorageAndCallback<T>>[2];
export async function createStorageAsync<T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): Promise<T> {
return new Promise(r => createStorageAndCallback(path, r, opts));
}
export const createStorage = <T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): T => {
const promise = new Promise(r => resolvePromise = r);
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
createStorageAndCallback(path, proxy => {
awaited = proxy;
resolved = true;
resolvePromise();
}, opts);
const check = () => {
if (resolved) return true;
throw new Error(`Attempted to access storage without initializing: ${path}`);
};
return new Proxy({} as any, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
// @ts-expect-error
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
.map(k => [k, (t: T, ...a: any[]) => {
// @ts-expect-error
return check() && Reflect[k](awaited, ...a);
}])
),
get(target, prop, recv) {
if (prop === syncAwaitSymbol) return awaitInit;
return Reflect.get(awaited ?? target, prop, recv);
if (prop === storageInitErrorSymbol) return error;
if (prop === storagePromiseSymbol) return promise;
return check() && Reflect.get(awaited ?? target, prop, recv);
},
});
};
export async function preloadStorageIfExists(path: string): Promise<boolean> {
if (_loadedStorage[path]) return true;
const backend = createFileBackend(path);
if (await backend.exists()) {
_loadedStorage[path] = await backend.get();
return false;
}
return true;
}
export function awaitStorage(...stores: any[]) {
return Promise.all(
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
);
export async function purgeStorage(path: string) {
await removeFile(path);
delete _loadedStorage[path];
}
export {
createFileBackend,
createMMKVBackend,
purgeStorage
} from "@lib/api/storage/backends";
export function awaitStorage(...proxies: any[]) {
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
}
/** @internal */
export function getPreloadedStorage<T extends object = {}>(path: string): T | undefined {
return _loadedStorage[path];
}

View file

@ -1,205 +0,0 @@
// New iteration of storage API, mostly yoinked from unreleased pyoncord (and sunrise?)
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
import { Emitter } from "@lib/utils/Emitter";
interface StorageBackend<T = unknown> {
get: () => Promise<T>;
set: (data: T) => Promise<void>;
exists: () => Promise<boolean>;
}
const emitterSymbol = Symbol.for("bunny.storage.emitter");
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
const _loadedPath = {} as Record<string, any>;
function createFileBackend<T = any>(filePath: string): StorageBackend<T> {
return {
get: async () => {
try {
return JSON.parse(await readFile(filePath));
} catch (e) {
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
}
},
set: async data => {
if (!data || typeof data !== "object") throw new Error("data needs to be an object");
await writeFile(filePath, JSON.stringify(data));
},
exists: async () => {
return await fileExists(filePath);
}
};
}
function _createProxy(target: any, path: string[], emitter: Emitter): any {
// cache the children proxies so proxy.child === proxy.child (yields the same value)
const objChildrens = new WeakMap<object, object>();
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
let value: any = target[prop];
if (value && typeof value === "object") {
const origValue = value;
value = objChildrens.get(origValue);
if (!value) {
value = _createProxy(origValue, newPath, emitter);
objChildrens.set(origValue, value!);
}
}
if (value != null) {
emitter.emit("GET", {
path: newPath,
value,
});
}
return value;
},
set(target, prop: string, value) {
target[prop] = value;
emitter.emit("SET", {
path: [...path, prop],
value,
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
path: [...path, prop],
});
return success;
},
});
}
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
return {
proxy: _createProxy(target, [], emitter),
emitter,
};
}
export function useProxy<T>(storage: T & { [key: string | symbol]: any; }) {
if (storage[storageInitErrorSymbol]) throw new Error(
"An error occured while initializing the storage",
{ cause: storage[storageInitErrorSymbol] }
);
const emitter = storage[emitterSymbol] as Emitter;
if (emitter == null) {
throw new Error(`InvalidArgumentException - storage[emitterSymbol] is ${typeof emitter}`);
}
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener = () => forceUpdate();
emitter.on("SET", listener);
emitter.on("DEL", listener);
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
};
}, []);
}
export async function updateStorageAsync<T>(path: string, value: T): Promise<void> {
_loadedPath[path] = value;
await createFileBackend<T>(path).set(value);
}
export function createStorageAndCallback<T>(path: string, dflt = {} as T, cb: (proxy: T) => void) {
const callback = (data: any) => {
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
cb(proxy);
};
const backend = createFileBackend<T>(path);
if (_loadedPath[path]) callback(_loadedPath[path]);
else {
backend.exists().then(async exists => {
if (!exists) {
await backend.set(dflt);
callback(dflt);
} else {
callback(await backend.get());
}
});
}
}
export async function createStorageAsync<T>(path: string, dflt = {} as T): Promise<T> {
return new Promise(r => createStorageAndCallback(path, dflt, r));
}
export const createStorage = <T>(path: string, dflt = {} as T): T & { [key: symbol]: any; } => {
const promise = new Promise(r => resolvePromise = r);
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
createStorageAndCallback(path, dflt, proxy => {
awaited = proxy;
resolved = true;
resolvePromise();
});
const check = () => {
if (resolved) return true;
throw new Error("Attempted to access storage without initializing");
};
return new Proxy({} as any, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
.map(k => [k, (t: T, ...a: any[]) => {
// @ts-expect-error
return check() && Reflect[k](awaited, ...a);
}])
),
get(target, prop, recv) {
if (prop === storageInitErrorSymbol) return error;
if (prop === storagePromiseSymbol) return promise;
return check() && Reflect.get(awaited ?? target, prop, recv);
},
});
};
export async function preloadStorageIfExists(path: string) {
if (_loadedPath[path]) return _loadedPath[path];
const backend = createFileBackend(path);
if (await backend.exists()) {
return _loadedPath[path] = await backend.get();
}
console.log("no " + path);
}
export function getPreloadedStorage<T>(path: string): T {
return _loadedPath[path];
}
export function awaitStorage(...proxies: any[]) {
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
}

View file

@ -1,19 +1,27 @@
import "../global.d.ts"; // eslint-disable-line import-alias/import-alias
import "../modules.d.ts"; // eslint-disable-line import-alias/import-alias
export * as fonts from "./addons/fonts/index.js";
export * as plugins from "./addons/plugins/index.js";
export * as themes from "./addons/themes/index.js";
export * as api from "./api";
export * as fonts from "./fonts";
export * as plugins from "./plugins";
export * as themes from "./themes";
export * as ui from "./ui";
export * as utils from "./utils";
export * as metro from "@metro";
import * as fonts from "./fonts";
import * as plugins from "./plugins";
import * as themes from "./themes";
import * as fonts from "./addons/fonts/index.js";
import * as plugins from "./addons/plugins/index.js";
import * as themes from "./addons/themes/index.js";
/** @internal */
export * as _jsx from "react/jsx-runtime";
import { proxyLazy } from "./utils/lazy";
/**
* @internal
* @deprecated Moved to top level (bunny.*)
*/
export const managers = proxyLazy(() => {
console.warn("bunny.managers.* is deprecated, and moved the top level (bunny.*). bunny.managers will be eventually removed soon");

View file

@ -1,4 +1,4 @@
import { color } from "@lib/themes";
import { color } from "@lib/addons/themes";
import { constants } from "@metro/common";
import { findByStoreNameLazy } from "@metro/wrappers";

View file

@ -1,10 +1,9 @@
import { Strings } from "@core/i18n";
import { Nullish } from "@lib/utils/types";
import { React } from "@metro/common";
import { Button, LegacyFormText } from "@metro/common/components";
import { Codeblock } from "@ui/components";
import { createThemedStyleSheet } from "@ui/styles";
import { ScrollView } from "react-native";
import { Falsy, ScrollView } from "react-native";
type ErrorBoundaryState = {
hasErr: false;
@ -14,7 +13,7 @@ type ErrorBoundaryState = {
};
export interface ErrorBoundaryProps {
children: JSX.Element | Nullish | (JSX.Element | Nullish)[];
children: JSX.Element | Falsy | (JSX.Element | Falsy)[];
}
const styles = createThemedStyleSheet({

View file

@ -1,4 +1,3 @@
import { ButtonColors } from "@lib/utils/types";
import { LegacyAlert, LegacyFormInput } from "@metro/common/components";
import { findByPropsLazy } from "@metro/wrappers";
@ -7,7 +6,7 @@ const Alerts = findByPropsLazy("openLazy", "close");
export interface InputAlertProps {
title?: string;
confirmText?: string;
confirmColor?: ButtonColors;
confirmColor?: string;
onConfirm: (input: string) => (void | Promise<void>);
cancelText?: string;
placeholder?: string;

View file

@ -1,3 +1,4 @@
export * as wrappers from "./wrappers";
export { default as Codeblock } from "@ui/components/Codeblock";
export { default as ErrorBoundary } from "@ui/components/ErrorBoundary";
export { default as Search } from "@ui/components/Search";

View file

@ -0,0 +1,39 @@
import { lazyDestructure } from "@lib/utils/lazy";
import { findByFilePath, findByProps } from "@metro";
import { Text } from "@metro/common/components";
import { Button } from "@metro/common/types/components";
import { ComponentProps, ComponentType, ReactNode } from "react";
import { View } from "react-native";
type ActionButtonProps = Omit<ComponentProps<Button>, "onPress"> & {
onPress?: () => void | Promise<unknown>;
};
const {
AlertModal: _AlertModal,
AlertActionButton: _AlertActionButton
} = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
export const AlertActionButton = _AlertActionButton as ComponentType<ActionButtonProps>;
export default function AlertModal(props: Record<string, unknown>) {
const forwardFailedModal = findByFilePath("modules/forwarding/native/ForwardFailedAlertModal.tsx");
// ponyfill for extraContent
if (!forwardFailedModal && "extraContent" in props) {
props.content = (
<View style={{ gap: 16 }}>
<Text variant="text-md/medium" color="text-muted">
{props.content as string}
</Text>
<View>
{props.extraContent as ReactNode}
</View>
</View>
);
delete props.extraContent;
}
return <_AlertModal {...props} />;
}

View file

@ -0,0 +1,3 @@
import AlertModal, { AlertActionButton } from "./AlertModal";
export { AlertActionButton, AlertModal };

View file

@ -1,7 +1,6 @@
export * as alerts from "./alerts";
export * as color from "./color";
export * as components from "./components";
export * as settings from "./settings";
export * as sheets from "./sheets";
export * as styles from "./styles";
export * as toasts from "./toasts";

View file

@ -4,7 +4,6 @@ import { DeviceManager } from "@lib/api/native/modules";
import { after } from "@lib/api/patcher";
import { settings } from "@lib/api/settings";
import { lazyDestructure } from "@lib/utils/lazy";
import { ButtonColors } from "@lib/utils/types";
import { Button, CompatButton, SafeAreaView } from "@metro/common/components";
import { _lazyContextSymbol } from "@metro/lazy";
import { LazyModuleContext } from "@metro/types";
@ -91,7 +90,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
const buttons: Button[] = [
{ text: Strings.RELOAD_DISCORD, onPress: this.handleReload },
...!settings.safeMode?.enabled ? [{ text: Strings.RELOAD_IN_SAFE_MODE, onPress: toggleSafeMode }] : [],
{ text: Strings.RETRY_RENDER, color: ButtonColors.RED, onPress: () => this.setState({ info: null, error: null }) },
{ text: Strings.RETRY_RENDER, color: "red", onPress: () => this.setState({ info: null, error: null }) },
];
return (
@ -130,7 +129,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
return <CompatButton
text={button.text}
color={button.color ?? ButtonColors.BRAND}
color={button.color ?? "brand"}
size={button.size ?? "small"}
onPress={button.onPress}
style={{

View file

@ -12,6 +12,7 @@ export interface RowConfig {
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element,
rawTabsConfig?: Record<string, any>;
}

View file

@ -1,7 +1,7 @@
import { after } from "@lib/api/patcher";
import { findInReactTree } from "@lib/utils";
import { i18n, NavigationNative } from "@metro/common";
import { LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
import { LegacyFormDivider,LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
import { findByNameLazy } from "@metro/wrappers";
import { registeredSections } from "@ui/settings";
@ -12,16 +12,19 @@ function SettingsSection() {
const navigation = NavigationNative.useNavigation();
return <>
{Object.keys(registeredSections).map(sect => (
{Object.keys(registeredSections).map(sect => registeredSections[sect].length > 0 && (
<LegacyFormSection key={sect} title={sect}>
{ /** Is usePredicate here safe? */}
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map(row => (
<LegacyFormRow
label={row.title()}
leading={<LegacyFormIcon source={row.icon} />}
trailing={LegacyFormRow.Arrow}
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
/>
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map((row, i, arr) => (
<>
<LegacyFormRow
label={row.title()}
leading={<LegacyFormIcon source={row.icon} />}
trailing={<LegacyFormRow.Arrow label={row.useTrailing?.() || undefined} />}
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
/>
{i !== arr.length - 1 && <LegacyFormDivider />}
</>
))}
</LegacyFormSection>
))}
@ -55,7 +58,7 @@ export function patchPanelUI(unpatches: (() => void | boolean)[]) {
const sections = findInReactTree(
res.props.children,
n => n?.children?.[1]?.type === LegacyFormSection
)?.children;
)?.children || res.props.children;
if (sections) {
const index = sections.findIndex((c: any) => titles.includes(c?.props.label));

View file

@ -24,6 +24,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
title: row.title,
icon: row.icon,
usePredicate: row.usePredicate,
useTrailing: row.useTrailing,
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
withArrow: true,
...row.rawTabsConfig

View file

@ -1,163 +1,3 @@
import { OptionalKeys } from "@lib/utils/types";
import { LiteralUnion } from "type-fest";
export type DiscordTextStyles = OptionalKeys<[
"heading-sm/normal",
"heading-sm/medium",
"heading-sm/semibold",
"heading-sm/bold",
"heading-sm/extrabold",
"heading-md/normal",
"heading-md/medium",
"heading-md/semibold",
"heading-md/bold",
"heading-md/extrabold",
"heading-lg/normal",
"heading-lg/medium",
"heading-lg/semibold",
"heading-lg/bold",
"heading-lg/extrabold",
"heading-xl/normal",
"heading-xl/medium",
"heading-xl/semibold",
"heading-xl/bold",
"heading-xl/extrabold",
"heading-xxl/normal",
"heading-xxl/medium",
"heading-xxl/semibold",
"heading-xxl/bold",
"heading-xxl/extrabold",
"eyebrow",
"heading-deprecated-12/normal",
"heading-deprecated-12/medium",
"heading-deprecated-12/semibold",
"heading-deprecated-12/bold",
"heading-deprecated-12/extrabold",
"redesign/heading-18/bold",
"text-xxs/normal",
"text-xxs/medium",
"text-xxs/semibold",
"text-xxs/bold",
"text-xs/normal",
"text-xs/medium",
"text-xs/semibold",
"text-xs/bold",
"text-sm/normal",
"text-sm/medium",
"text-sm/semibold",
"text-sm/bold",
"text-md/normal",
"text-md/medium",
"text-md/semibold",
"text-md/bold",
"text-lg/normal",
"text-lg/medium",
"text-lg/semibold",
"text-lg/bold",
"redesign/message-preview/normal",
"redesign/message-preview/medium",
"redesign/message-preview/semibold",
"redesign/message-preview/bold",
"redesign/channel-title/normal",
"redesign/channel-title/medium",
"redesign/channel-title/semibold",
"redesign/channel-title/bold",
"display-sm",
"display-md",
"display-lg"
]>;
export type RedesignModuleKeys = OptionalKeys<[
"AlertActionButton",
"AlertModal",
"AlertModalContainer",
"AvatarDuoPile",
"AvatarPile",
"BACKDROP_OPAQUE_MAX_OPACITY",
"Backdrop",
"Button",
"Card",
"ContextMenu",
"ContextMenuContainer",
"FauxHeader",
"FloatingActionButton",
"GhostInput",
"GuildIconPile",
"HeaderActionButton",
"HeaderButton",
"HeaderSubmittingIndicator",
"IconButton",
"Input",
"InputButton",
"InputContainer",
"LayerContext",
"LayerScope",
"Modal",
"ModalActionButton",
"ModalContent",
"ModalDisclaimer",
"ModalFloatingAction",
"ModalFloatingActionSpacer",
"ModalFooter",
"ModalScreen",
"ModalStepIndicator",
"NAV_BAR_HEIGHT",
"NAV_BAR_HEIGHT_MULTILINE",
"Navigator",
"NavigatorHeader",
"NavigatorScreen",
"Pile",
"PileOverflow",
"RedesignCompat",
"RedesignCompatContext",
"RowButton",
"STATUS_BAR_HEIGHT",
"SceneLoadingIndicator",
"SearchField",
"SegmentedControl",
"SegmentedControlPages",
"Slider",
"Stack",
"StepModal",
"StickyContext",
"StickyHeader",
"StickyWrapper",
"TABLE_ROW_CONTENT_HEIGHT",
"TABLE_ROW_HEIGHT",
"TableCheckboxRow",
"TableRadioGroup",
"TableRadioRow",
"TableRow",
"TableRowGroup",
"TableRowGroupTitle",
"TableRowIcon",
"TableSwitchRow",
"Tabs",
"TextArea",
"TextField",
"TextInput",
"Toast",
"dismissAlerts",
"getHeaderBackButton",
"getHeaderCloseButton",
"getHeaderConditionalBackButton",
"getHeaderNoTitle",
"getHeaderTextButton",
"hideContextMenu",
"navigatorShouldCrossfade",
"openAlert",
"useAccessibilityNativeStackOptions",
"useAndroidNavScrim",
"useCoachmark",
"useFloatingActionButtonScroll",
"useFloatingActionButtonState",
"useNativeStackNavigation",
"useNavigation",
"useNavigationTheme",
"useNavigatorBackPressHandler",
"useNavigatorScreens",
"useNavigatorShouldCrossfade",
"useSegmentedControlState",
"useStackNavigation",
"useTabNavigation",
"useTooltip"
]>;
export type DiscordTextStyles = LiteralUnion<"heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg", string>;

View file

@ -2,13 +2,12 @@ export const DISCORD_SERVER = "https://discord.com/invite/ddcQf3s2Uq";
export const GITHUB = "https://github.com/revenge-mod";
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;
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
export const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json";
// These are still Vendetta's
export const DISCORD_SERVER_ID = "1015931589865246730";
export const PLUGINS_CHANNEL_ID = "1091880384561684561";
export const THEMES_CHANNEL_ID = "1091880434939482202";
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
export const VD_DISCORD_SERVER_ID = "1015931589865246730";
export const VD_PLUGINS_CHANNEL_ID = "1091880384561684561";
export const VD_THEMES_CHANNEL_ID = "1091880434939482202";

View file

@ -3,7 +3,7 @@
// 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) {
export default 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);

View file

@ -1,6 +1,8 @@
import { findInTree } from "@lib/utils";
import { SearchFilter } from "@lib/utils/findInTree";
export const findInReactTree = (tree: { [key: string]: any; }, filter: SearchFilter): any => findInTree(tree, filter, {
walkable: ["props", "children", "child", "sibling"],
});
export default function findInReactTree(tree: { [key: string]: any; }, filter: SearchFilter): any {
return findInTree(tree, filter, {
walkable: ["props", "children", "child", "sibling"],
});
}

View file

@ -15,7 +15,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
if (filter(tree)) return tree;
} catch {}
} catch { }
if (Array.isArray(tree)) {
for (const item of tree) {
@ -24,7 +24,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
const found = treeSearch(item, filter, opts, depth + 1);
if (found) return found;
} catch {}
} catch { }
}
} else if (typeof tree === "object") {
for (const key of Object.keys(tree)) {
@ -35,12 +35,12 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
const found = treeSearch(tree[key], filter, opts, depth + 1);
if (found) return found;
} catch {}
} catch { }
}
}
}
export const findInTree = (
export default function findInTree(
tree: SearchTree,
filter: SearchFilter,
{
@ -48,4 +48,6 @@ export const findInTree = (
ignore = [],
maxDepth = 100
}: FindInTreeOptions = {},
): any | undefined => treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
): any | undefined {
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
}

View file

@ -2,7 +2,7 @@ import { LiteralUnion } from "type-fest";
type KeyOfOrAny<P, T extends object> = P extends keyof T ? T[P] : any;
export function hookDefineProperty<
export default function hookDefineProperty<
T extends object,
P extends LiteralUnion<keyof T, PropertyKey>
>(target: T, property: LiteralUnion<keyof T, PropertyKey>, cb: (val: KeyOfOrAny<P, T>) => KeyOfOrAny<P, T>) {

View file

@ -1,10 +1,20 @@
export * as constants from "./constants";
export { cyrb64 } from "./cyrb64";
export { Emitter } from "./Emitter";
export { findInReactTree } from "./findInReactTree";
export { findInTree } from "./findInTree";
export { hookDefineProperty } from "./hookDefineProperty";
export { default as invariant } from "./invariant";
export * as lazy from "./lazy";
export * as logger from "./logger";
export { safeFetch } from "./safeFetch";
import * as constants from "./constants";
import cyrb64 from "./cyrb64";
import findInReactTree from "./findInReactTree";
import findInTree from "./findInTree";
import hookDefineProperty from "./hookDefineProperty";
import invariant from "./invariant";
import * as lazy from "./lazy";
import * as logger from "./logger";
import safeFetch from "./safeFetch";
export {
constants,
cyrb64,
findInReactTree,
findInTree,
hookDefineProperty,
invariant,
lazy,
logger,
safeFetch };

View file

@ -1,6 +1,6 @@
import { findByNameLazy } from "@metro/wrappers";
export type LoggerFunction = (...messages: any[]) => void;
type LoggerFunction = (...messages: any[]) => void;
export interface Logger {
log: LoggerFunction;
info: LoggerFunction;

View file

@ -1,5 +1,5 @@
// A really basic fetch wrapper which throws on non-ok response codes
export async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
export default async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
const req = await fetch(input, {
signal: timeoutSignal(timeout),
...options

View file

@ -1,22 +0,0 @@
import { LiteralUnion } from "type-fest";
export type Nullish = null | undefined;
export type OptionalKeys<T extends Array<string>> = LiteralUnion<T[number], string>;
export interface Author {
name: string;
id?: string;
}
export enum ButtonColors {
BRAND = "brand",
RED = "red",
GREEN = "green",
PRIMARY = "primary",
TRANSPARENT = "transparent",
GREY = "grey",
LIGHTGREY = "lightgrey",
WHITE = "white",
LINK = "link"
}

View file

@ -2,6 +2,8 @@ import { findByFilePathLazy, findByProps, findByPropsLazy } from "@metro/wrapper
import type { Dispatcher } from "./types/flux";
export * as components from "./components";
// Discord
export const constants = findByPropsLazy("Fonts", "Permissions");
export const channels = findByPropsLazy("getVoiceChannelId");

View file

@ -1,4 +1,3 @@
import { Nullish } from "@lib/utils/types";
import { DiscordTextStyles } from "@ui/types";
import { MutableRefObject, ReactNode, RefObject } from "react";
import type * as RN from "react-native";
@ -25,6 +24,7 @@ interface ButtonProps {
scaleAmountInPx?: number;
icon?: ImageSourcePropType | ReactNode;
style?: Style;
grow?: boolean;
}
export type Button = React.ForwardRefExoticComponent<ButtonProps>;
@ -103,7 +103,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
trailingIcon?: React.FC<any>;
trailingPressableProps?: PressableProps;
trailingText?: string;
value?: string | Nullish;
value?: string | RN.Falsy;
}
export type TextInput = React.FC<TextInputProps>;

View file

@ -100,7 +100,6 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) {
patchedNativeComponentRegistry = true;
}
// Hook DeveloperExperimentStore
if (moduleExports?.default?.constructor?.displayName === "DeveloperExperimentStore") {
moduleExports.default = new Proxy(moduleExports.default, {

View file

@ -1,4 +1,4 @@
import type { Nullish } from "@types";
type Nullish = null | undefined;
/** @see {@link https://github.com/facebook/metro/blob/c2d7539dfc10aacb2f99fcc2f268a3b53e867a90/packages/metro-runtime/src/polyfills/require.js} */
export namespace Metro {