Merge branch 'kmmiio99o-main'

This commit is contained in:
cocobo1 2025-10-11 17:20:39 +03:00
commit a86888e2c0
13 changed files with 880 additions and 495 deletions

View file

@ -1,22 +1,25 @@
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
interface CorePlugin {
default: PluginInstanceInternal;
preenabled: boolean;
default: PluginInstanceInternal;
preenabled: boolean;
}
// Called from @lib/plugins
export const getCorePlugins = (): Record<string, CorePlugin> => ({
"bunny.quickinstall": require("./quickinstall"),
"bunny.badges": require("./badges"),
"vendetta.notrack": require("./notrack")
"bunny.quickinstall": require("./quickinstall"),
"bunny.badges": require("./badges"),
"bunny.notrack": { ...require("./notrack"), preenabled: true },
"bunny.messagefix": { default: require("./messagefix"), preenabled: true },
});
/**
* @internal
*/
export function defineCorePlugin(instance: PluginInstanceInternal): PluginInstanceInternal {
// @ts-expect-error
instance[Symbol.for("bunny.core.plugin")] = true;
return instance;
export function defineCorePlugin(
instance: PluginInstanceInternal,
): PluginInstanceInternal {
// @ts-expect-error
instance[Symbol.for("bunny.core.plugin")] = true;
return instance;
}

View file

@ -0,0 +1,56 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
// Find the MessageActions module
const MessageActions = findByProps("sendMessage");
let originalSendMessage: Function;
export default defineCorePlugin({
manifest: {
id: "bunny.messagefix",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "MessageFix",
description: "Ensures messages include the required nonce parameter",
authors: [{ name: "ShiggyCord Team" }],
},
},
start() {
// Save original function
originalSendMessage = MessageActions.sendMessage;
// Replace with our implementation
MessageActions.sendMessage = function (
channelId,
message,
replyRef,
options,
) {
// Ensure options exists and has a nonce
options = options || {};
options.nonce = options.nonce || Date.now().toString();
// Call original with fixed parameters
return originalSendMessage.call(
this,
channelId,
message,
replyRef,
options,
);
};
logger.log("MessageFix: Enabled - adding nonce to all messages");
},
stop() {
// Restore original function
if (originalSendMessage) MessageActions.sendMessage = originalSendMessage;
logger.log("MessageFix: Disabled");
},
});

View file

@ -1,36 +1,230 @@
import network from "./lib/network";
import console from "./lib/console";
import miscellanous from "./lib/miscellanous";
import sentry from "./lib/sentry";
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { instead } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
const patches = [
network(),
console(),
miscellanous(),
// Sentry causes issues
// sentry(),
];
// Type for functions that need to be cleaned up when plugin is disabled
type PatchCleanupFn = () => void;
// Store all active patches for cleanup
let patches: PatchCleanupFn[] = [];
// AnalyticsUtils and other tracking utilities
const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
const SuperPropUtils = findByProps("encodeProperties", "track");
const VoiceStateUtils = findByProps("getVoiceStateMetadata");
const CrashReportUtils = findByProps("submitLiveCrashReport");
const MetricsUtils = findByProps("_metrics");
// Sentry interface definition to avoid TypeScript errors
interface SentryClient {
getOptions: () => { enabled: boolean };
close: () => void;
open: () => void;
}
interface SentryScope {
clear: () => void;
}
interface SentryHub {
getClient: () => SentryClient | undefined;
getStackTop: () => { scope: SentryScope };
getScope: () => SentryScope;
addBreadcrumb?: (breadcrumb: unknown) => void;
}
interface GlobalSentry {
hub?: SentryHub;
}
// Get Sentry objects with proper type checking
const sentryGlobal = (globalThis as unknown as { __SENTRY__?: GlobalSentry })
.__SENTRY__;
const sentryHub = sentryGlobal?.hub;
const sentryClient = sentryHub?.getClient();
// Sentry related objects with type safety
const Sentry = {
initializer: findByProps("initSentry"),
main: sentryHub,
client: sentryClient,
};
// Utility function to create no-operation patches
const noop = (prop: string, parent: Record<string, any>): PatchCleanupFn => {
try {
return instead(prop, parent, () => undefined);
} catch (e) {
return () => false;
}
};
// Network tracking prevention
function patchNetwork(): PatchCleanupFn {
// Analytics and tracking endpoints to block
const analyticsTest =
/client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
try {
const unpatch = instead(
"send",
XMLHttpRequest.prototype,
function (
this: XMLHttpRequest & { __sentry_xhr__?: { url: string } },
args: unknown[],
orig: Function,
) {
if (
this.__sentry_xhr__?.url &&
analyticsTest.test(this.__sentry_xhr__.url)
) {
return undefined;
}
return orig.apply(this, args);
},
);
return unpatch;
} catch (e) {
return () => false;
}
}
// Console patching to remove Sentry wrappers
function patchConsole(): PatchCleanupFn {
type ConsoleFunction = Function & { __sentry_original__?: Function };
type ConsoleFunctions = Record<string, ConsoleFunction>;
const sentrified: ConsoleFunctions = {};
try {
// We need to use Object.keys because TypeScript doesn't allow indexing Console with strings
(Object.keys(console) as Array<keyof Console>).forEach((key) => {
const consoleFunc = (console as unknown as ConsoleFunctions)[key];
if (consoleFunc) {
sentrified[key] = consoleFunc;
const originalFunc = consoleFunc.__sentry_original__;
(console as unknown as ConsoleFunctions)[key] =
originalFunc ?? consoleFunc;
}
});
} catch (e) {
logger.log("Failed to de-sentrify console functions!", e);
}
return () => {
Object.keys(sentrified).forEach((key) => {
if (sentrified[key]) {
(console as unknown as ConsoleFunctions)[key] = sentrified[key];
}
});
};
}
// Miscellaneous tracking functions
function patchMiscellaneous(): PatchCleanupFn {
const miscPatches = [
// Global analytics utilities
AnalyticsUtils?.AnalyticsActionHandlers &&
noop("handleTrack", AnalyticsUtils.AnalyticsActionHandlers),
AnalyticsUtils?.AnalyticsActionHandlers &&
noop("handleFingerprint", AnalyticsUtils.AnalyticsActionHandlers),
// Super properties tracking
SuperPropUtils && noop("track", SuperPropUtils),
// Voice state metadata tracking
VoiceStateUtils && noop("trackWithMetadata", VoiceStateUtils),
// Crash reporter
CrashReportUtils && noop("submitLiveCrashReport", CrashReportUtils),
// Metrics
MetricsUtils?._metrics && noop("push", MetricsUtils._metrics),
].filter(Boolean) as PatchCleanupFn[];
return () => miscPatches.forEach((p) => p());
}
// Sentry error reporting prevention
function patchSentry(): PatchCleanupFn {
const sentryPatches: PatchCleanupFn[] = [];
// Add patches if Sentry components exist
if (Sentry.initializer) {
sentryPatches.push(noop("initSentry", Sentry.initializer));
}
if (Sentry.main && Sentry.main.addBreadcrumb) {
sentryPatches.push(noop("addBreadcrumb", Sentry.main));
}
// Disable Sentry client
if (Sentry.client) {
try {
Sentry.client.getOptions().enabled = false;
Sentry.client.close();
if (Sentry.main) {
if (Sentry.main.getStackTop) {
Sentry.main.getStackTop().scope.clear();
}
if (Sentry.main.getScope) {
Sentry.main.getScope().clear();
}
}
} catch (e) {
// Silent fail
}
}
return () => {
try {
sentryPatches.forEach((p) => p());
// Re-enable Sentry client on cleanup
if (Sentry.client) {
Sentry.client.getOptions().enabled = true;
Sentry.client.open();
}
} catch (e) {
// Silent fail
}
};
}
export default defineCorePlugin({
manifest: {
id: "vendetta.notrack",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "NoTrack",
description: "Disables Discord's telemetry",
authors: [{ name: "maisymoe" }]
}
manifest: {
id: "bunny.notrack",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "NoTrack",
description: "Disables Discord's telemetry",
authors: [{ name: "maisymoe" }],
},
start() {
network(),
console(),
miscellanous(),
sentry()
}
});
},
start() {
// Apply all patches
patches = [
patchNetwork(),
patchConsole(),
patchMiscellaneous(),
patchSentry(),
].filter(Boolean);
logger.log("NoTrack: Enabled - all telemetry tracking disabled");
},
stop() {
// Call all cleanup functions
patches.forEach((p) => p?.());
patches = [];
logger.log("NoTrack: Disabled - telemetry tracking restored");
},
});

View file

@ -1,16 +0,0 @@
import { logger } from "@lib/utils/logger";
export default () => {
const sentrified = {};
try {
Object.keys(console).forEach((x) => {
sentrified[x] = console[x];
console[x] = console[x].__sentry_original__ ?? console[x];
});
} catch(e) {
logger.log("Failed to de-sentrify console functions!", e);
};
return () => Object.keys(console).forEach((x) => console[x] = sentrified[x]);
}

View file

@ -1,33 +0,0 @@
import { findByProps } from "@metro";
import { noop } from "./utils";
// Stuff in here is non-specific tracking scattered around the app
const AnalyticsUtils = findByProps("AnalyticsActionHandlers");
const SuperPropUtils = findByProps("encodeProperties", "track");
const VoiceStateUtils = findByProps("getVoiceStateMetadata");
const CrashReportUtils = findByProps("submitLiveCrashReport");
const MetricsUtils = findByProps("_metrics");
export default () => {
const patches = [
// Global analytics utilities
...(["handleTrack", "handleFingerprint"].map((i) => noop(i, AnalyticsUtils.AnalyticsActionHandlers))),
// TODO: This is a getter - look into patching it
// noop("analyticsTrackingStoreMaker", AnalyticsUtils),
// Super properties tracking
noop("track", SuperPropUtils),
// Voice state metadata tracking
noop("trackWithMetadata", VoiceStateUtils),
// Crash reporter
noop("submitLiveCrashReport", CrashReportUtils),
// Whatever "metrics" implies
noop("push", MetricsUtils._metrics),
];
return () => patches.forEach((p) => p());
}

View file

@ -1,12 +0,0 @@
import { instead } from "@lib/api/patcher";
export default () => {
// https://github.com/yellowsink/shelter-plugins/blob/3a62d1557b704f4617f72882edba826218eb8c6e/plugins/antitrack/index.js#L10-L25
const analyticsTest = /client-analytics\.braintreegateway\.com|discord\.com\/api\/v9\/(science|track)|app\.adjust\..*|.*\.ingest\.sentry\.io/;
return instead(
"send",
XMLHttpRequest.prototype,
function (args, orig) { if (!analyticsTest.test(this.__sentry_xhr__?.url)) return orig.apply(this, args) },
);
}

View file

@ -1,26 +0,0 @@
import { findByProps } from "@metro";
import { noop } from "./utils";
const Sentry = {
initializer: findByProps("initSentry"),
main: globalThis.__SENTRY__?.hub,
client: globalThis.__SENTRY__?.hub?.getClient(),
};
export default () => {
const patches = [
noop("initSentry", Sentry.initializer),
noop("addBreadcrumb", Sentry.main),
];
Sentry.client.getOptions().enabled = false;
Sentry.client.close();
Sentry.main.getStackTop().scope.clear();
Sentry.main.getScope().clear();
return () => {
patches.forEach((p) => p());
Sentry.client.getOptions().enabled = true;
Sentry.client.open();
};
}

View file

@ -1,3 +0,0 @@
import { instead } from "@lib/api/patcher";
export const noop = (prop: string, parent: any) => instead(prop, parent, () => {});

View file

@ -3,154 +3,182 @@ import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models";
import { usePluginCardStyles } from "@core/ui/settings/pages/Plugins/usePluginCardStyles";
import { findAssetId } from "@lib/api/assets";
import { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import {
Card,
IconButton,
Stack,
TableSwitch,
Text,
} from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
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 {
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
return chroma(tokens.unsafe_rawColors.YELLOW_300).alpha(0.3).hex();
}
function Title() {
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
// could be empty if the plugin name is irrelevant!
const highlightedNode = result[0].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
// could be empty if the plugin name is irrelevant!
const highlightedNode = result[0].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
const icon = plugin.icon && findAssetId(plugin.icon);
const icon = plugin.icon && findAssetId(plugin.icon);
const textNode = (
<Text
numberOfLines={1}
variant="heading-lg/semibold"
>
{highlightedNode.length ? highlightedNode : plugin.name}
</Text>
);
const textNode = (
<Text numberOfLines={1} variant="heading-lg/semibold">
{highlightedNode.length ? highlightedNode : plugin.name}
</Text>
);
return <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{icon && <Image
style={styles.smallIcon}
source={icon}
/>}
{textNode}
</View>;
return (
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
{icon && <Image style={styles.smallIcon} source={icon} />}
{textNode}
</View>
);
}
function Authors() {
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
const { plugin, result } = useCardContext();
const styles = usePluginCardStyles();
if (!plugin.authors) return null;
if (!plugin.authors) return null;
// could be empty if the author(s) are irrelevant with the search!
const highlightedNode = result[2].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
);
// could be empty if the author(s) are irrelevant with the search!
const highlightedNode = result[2].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
const badges = plugin.getBadges();
const authorText = highlightedNode.length > 0 ? highlightedNode : plugin.authors.map(a => a.name).join(", ");
const badges = plugin.getBadges();
const authorText =
highlightedNode.length > 0
? highlightedNode
: plugin.authors.map((a) => a.name).join(", ");
return (
<View style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}>
<Text variant="text-sm/semibold" color="text-muted">
by {authorText}
</Text>
{badges.length > 0 && <View style={styles.badgesContainer}>
{badges.map((b, i) => <Image
key={i}
source={b.source}
style={styles.badgeIcon}
/>)}
</View>}
return (
<View
style={{ flexDirection: "row", flexWrap: "wrap", flexShrink: 1, gap: 4 }}
>
<Text variant="text-sm/semibold" color="text-muted">
by {authorText}
</Text>
{badges.length > 0 && (
<View style={styles.badgesContainer}>
{badges.map((b, i) => (
<Image key={i} source={b.source} style={styles.badgeIcon} />
))}
</View>
);
)}
</View>
);
}
function Description() {
const { plugin, result } = useCardContext();
const { plugin, result } = useCardContext();
// could be empty if the description is irrelevant with the search!
const highlightedNode = result[1].highlight((m, i) =>
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>{m}</Text>
);
// could be empty if the description is irrelevant with the search!
const highlightedNode = result[1].highlight((m, i) => (
<Text key={i} style={{ backgroundColor: getHighlightColor() }}>
{m}
</Text>
));
return <Text variant="text-md/medium">
{highlightedNode.length ? highlightedNode : plugin.description}
</Text>;
return (
<Text variant="text-md/medium">
{highlightedNode.length ? highlightedNode : plugin.description}
</Text>
);
}
const Actions = () => {
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
return <View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("WrenchIcon")}
disabled={!plugin.getPluginSettingsComponent()}
onPress={() => navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
})}
/>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
onPress={() => void showSheet(
"PluginInfoActionSheet",
plugin.resolveSheetComponent(),
{ plugin, navigation }
)}
/>
</View>;
return (
<View style={{ flexDirection: "row", gap: 6 }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("WrenchIcon")}
disabled={!plugin.getPluginSettingsComponent()}
onPress={() =>
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
})
}
/>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
onPress={() =>
void showSheet(
"PluginInfoActionSheet",
plugin.resolveSheetComponent(),
{ plugin, navigation },
)
}
/>
</View>
);
};
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();
export default function PluginCard({
result,
item: plugin,
}: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();
const [, forceUpdate] = React.useReducer(() => ({}), 0);
const cardContextValue = useMemo(() => ({ plugin, result }), [plugin, result]);
const [, forceUpdate] = React.useReducer(() => ({}), 0);
const cardContextValue = useMemo(
() => ({ plugin, result }),
[plugin, result],
);
return (
<CardContext.Provider value={cardContextValue}>
<Card>
<Stack spacing={16}>
<View style={{ flexDirection: "row", justifyContent: "space-between" }}>
<View style={{ flexShrink: 1 }}>
<Title />
<Authors />
</View>
<View>
<Stack spacing={12} direction="horizontal">
<Actions />
<TableSwitch
value={plugin.isEnabled()}
onValueChange={(v: boolean) => {
plugin.toggle(v);
forceUpdate();
}}
/>
</Stack>
</View>
</View>
<Description />
</Stack>
</Card>
</CardContext.Provider>
);
return (
<CardContext.Provider value={cardContextValue}>
<Card>
<Stack spacing={16}>
<View
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<View style={{ flexShrink: 1 }}>
<Title />
<Authors />
</View>
<View>
<Stack spacing={12} direction="horizontal">
<Actions />
<TableSwitch
value={plugin.isEnabled()}
onValueChange={(v: boolean) => {
plugin.toggle(v);
forceUpdate();
}}
/>
</Stack>
</View>
</View>
<Description />
</Stack>
</Card>
</CardContext.Provider>
);
}

View file

@ -13,7 +13,13 @@ import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
import { NavigationNative } from "@metro/common";
import { Button, Card, FlashList, IconButton, Text } from "@metro/common/components";
import {
Button,
Card,
FlashList,
IconButton,
Text,
} from "@metro/common/components";
import { ComponentProps } from "react";
import { View } from "react-native";
import type { BunnyPluginManifest } from "@lib/addons/plugins/types";
@ -22,44 +28,53 @@ import { UnifiedPluginModel } from "./models";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const { openAlert } = lazyDestructure(() =>
findByProps("openAlert", "dismissAlert"),
);
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() =>
findByProps("AlertModal", "AlertActions"),
);
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
interface PluginPageProps
extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
}
function PluginPage(props: PluginPageProps) {
const items = props.useItems();
const items = props.useItems();
return <AddonPage<UnifiedPluginModel>
CardComponent={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
p => p.authors?.map(
(a: Author | string) => typeof a === "string" ? a : a.name
).join() || ""
]}
sortOptions={{
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name)
}}
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
items={items}
{...props}
/>;
return (
<AddonPage<UnifiedPluginModel>
CardComponent={PluginCard}
title={Strings.PLUGINS}
searchKeywords={[
"name",
"description",
(p) =>
p.authors
?.map((a: Author | string) => (typeof a === "string" ? a : a.name))
.join() || "",
]}
sortOptions={{
"Name (A-Z)": (a, b) => a.name.localeCompare(b.name),
"Name (Z-A)": (a, b) => b.name.localeCompare(a.name),
}}
safeModeHint={{ message: Strings.SAFE_MODE_NOTICE_PLUGINS }}
items={items}
{...props}
/>
);
}
export default function Plugins() {
useProxy(settings);
const navigation = NavigationNative.useNavigation();
useProxy(settings);
const navigation = NavigationNative.useNavigation();
return <PluginPage
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
return (
<PluginPage
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
const corePlugins = [...corePluginInstances.keys()]
.map((id) => registeredPlugins.get(id))
@ -74,68 +89,127 @@ export default function Plugins() {
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;
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("PUPU_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>}
/>;
}
});
}}
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("PUPU_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",
fetchFn: async (url: string) => {
if (!url.startsWith(VD_PROXY_PREFIX) && !url.startsWith(BUNNY_PROXY_PREFIX) && !settings.developerSettings) {
openAlert("bunny-plugin-unproxied-confirmation", <AlertModal
title="Hold On!"
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
extraContent={<Card><Text variant="text-md/bold">{url}</Text></Card>}
actions={<AlertActions>
<AlertActionButton text="Continue" variant="primary" onPress={() => {
VdPluginManager.installPlugin(url)
.then(() => showToast(Strings.TOASTS_INSTALLED_PLUGIN, findAssetId("Check")))
.catch(e => openAlert("bunny-plugin-install-failed", <AlertModal
title="Install Failed"
content={`Unable to install plugin from '${url}':`}
extraContent={<Card><Text variant="text-md/normal">{e instanceof Error ? e.message : String(e)}</Text></Card>}
actions={<AlertActionButton text="Okay" variant="primary" />}
/>));
}} />
<AlertActionButton text="Cancel" variant="secondary" />
</AlertActions>}
/>);
} else {
return await VdPluginManager.installPlugin(url);
);
},
});
}}
/>
</View>
</View>
</Card>
</View>
);
}}
installAction={{
label: "Install a plugin",
fetchFn: async (url: string) => {
if (
!url.startsWith(VD_PROXY_PREFIX) &&
!url.startsWith(BUNNY_PROXY_PREFIX) &&
!settings.developerSettings
) {
openAlert(
"bunny-plugin-unproxied-confirmation",
<AlertModal
title="Hold On!"
content="You're trying to install a plugin from an unproxied external source. This means you're trusting the creator to run their code in this app without your knowledge. Are you sure you want to continue?"
extraContent={
<Card>
<Text variant="text-md/bold">{url}</Text>
</Card>
}
}
}}
/>;
actions={
<AlertActions>
<AlertActionButton
text="Continue"
variant="primary"
onPress={() => {
VdPluginManager.installPlugin(url)
.then(() =>
showToast(
Strings.TOASTS_INSTALLED_PLUGIN,
findAssetId("Check"),
),
)
.catch((e) =>
openAlert(
"bunny-plugin-install-failed",
<AlertModal
title="Install Failed"
content={`Unable to install plugin from '${url}':`}
extraContent={
<Card>
<Text variant="text-md/normal">
{e instanceof Error
? e.message
: String(e)}
</Text>
</Card>
}
actions={
<AlertActionButton
text="Okay"
variant="primary"
/>
}
/>,
),
);
}}
/>
<AlertActionButton text="Cancel" variant="secondary" />
</AlertActions>
}
/>,
);
} else {
return await VdPluginManager.installPlugin(url);
}
},
}}
/>
);
}

View file

@ -1,43 +1,52 @@
import { PupuIcon } from "@core/ui/settings";
import { disablePlugin, enablePlugin, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
import {
disablePlugin,
enablePlugin,
getPluginSettingsComponent,
isPluginEnabled,
pluginSettings,
} from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { useObservable } from "@lib/api/storage";
import { UnifiedPluginModel } from ".";
export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): UnifiedPluginModel {
return {
id: manifest.id,
name: manifest.display.name,
description: manifest.display.description,
authors: manifest.display.authors,
export default function unifyBunnyPlugin(
manifest: BunnyPluginManifest,
): UnifiedPluginModel {
return {
id: manifest.id,
name: manifest.display.name,
description: manifest.display.description,
authors: manifest.display.authors,
getBadges() {
return [
{ source: { uri: PupuIcon } },
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
];
},
isEnabled: () => isPluginEnabled(manifest.id),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useObservable([pluginSettings]);
},
toggle(start: boolean) {
try {
start
? enablePlugin(manifest.id, true)
: disablePlugin(manifest.id);
} catch (e) {
console.error(e);
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
}
},
resolveSheetComponent() {
return import("../sheets/PluginInfoActionSheet");
},
getPluginSettingsComponent() {
return getPluginSettingsComponent(manifest.id);
},
};
getBadges() {
return [
{ source: { uri: PupuIcon } },
// { source: findAssetId("CheckmarkLargeBoldIcon")! }
];
},
isEnabled: () => isPluginEnabled(manifest.id),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useObservable([pluginSettings]);
},
toggle(start: boolean) {
try {
start ? enablePlugin(manifest.id, true) : disablePlugin(manifest.id);
} catch (e) {
console.error(e);
// showToast("Failed to toggle plugin " + e, findAssetId("Small"));
}
},
resolveSheetComponent() {
// Return a Promise resolving to the sheet component for interface consistency
return Promise.resolve({
default: require("../sheets/PluginInfoActionSheet").default,
});
},
getPluginSettingsComponent() {
return getPluginSettingsComponent(manifest.id);
},
};
}

View file

@ -3,32 +3,38 @@ import { useProxy } from "@core/vendetta/storage";
import { UnifiedPluginModel } from ".";
export default function unifyVdPlugin(vdPlugin: VendettaPlugin): UnifiedPluginModel {
return {
id: vdPlugin.id,
name: vdPlugin.manifest.name,
description: vdPlugin.manifest.description,
authors: vdPlugin.manifest.authors,
icon: vdPlugin.manifest.vendetta?.icon,
export default function unifyVdPlugin(
vdPlugin: VendettaPlugin,
): UnifiedPluginModel {
return {
id: vdPlugin.id,
name: vdPlugin.manifest.name,
description: vdPlugin.manifest.description,
authors: vdPlugin.manifest.authors,
icon: vdPlugin.manifest.vendetta?.icon,
getBadges() {
return [];
},
isEnabled: () => vdPlugin.enabled,
isInstalled: () => Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
usePluginState() {
useProxy(VdPluginManager.plugins[vdPlugin.id]);
},
toggle(start: boolean) {
start
? VdPluginManager.startPlugin(vdPlugin.id)
: VdPluginManager.stopPlugin(vdPlugin.id);
},
resolveSheetComponent() {
return import("../sheets/VdPluginInfoActionSheet");
},
getPluginSettingsComponent() {
return VdPluginManager.getSettings(vdPlugin.id);
},
};
getBadges() {
return [];
},
isEnabled: () => vdPlugin.enabled,
isInstalled: () =>
Boolean(vdPlugin && VdPluginManager.plugins[vdPlugin.id]),
usePluginState() {
useProxy(VdPluginManager.plugins[vdPlugin.id]);
},
toggle(start: boolean) {
start
? VdPluginManager.startPlugin(vdPlugin.id)
: VdPluginManager.stopPlugin(vdPlugin.id);
},
resolveSheetComponent() {
// Return a Promise resolving to the unified PluginInfoActionSheet for consistency
return Promise.resolve({
default: require("../sheets/PluginInfoActionSheet").default,
});
},
getPluginSettingsComponent() {
return VdPluginManager.getSettings(vdPlugin.id);
},
};
}

View file

@ -1,106 +1,211 @@
import { startPlugin } from "@lib/addons/plugins";
import { findAssetId } from "@lib/api/assets";
import { hideSheet } from "@lib/ui/sheets";
import { ActionSheet, Card, ContextMenu, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
import { ActionSheet, Card, IconButton, Text } from "@metro/common/components";
import { clipboard } from "@metro/common";
import { showToast } from "@lib/ui/toasts";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { purgeStorage as purgeVdStorage } from "@core/vendetta/storage";
import { purgeStorage } from "@lib/api/storage";
import { findAssetId } from "@lib/api/assets";
import { useState } from "react";
import { ScrollView, View } from "react-native";
import { PluginInfoActionSheetProps } from "./common";
import TitleComponent from "./TitleComponent";
import { PluginInfoActionSheetProps } from "./common";
function PluginInfoIconButton(props: ComponentProps<typeof IconButton>) {
const { onPress } = props;
props.onPress &&= () => {
function PluginInfoIconButton(props) {
const { onPress } = props;
props.onPress &&= () => {
hideSheet("PluginInfoActionSheet");
onPress?.();
};
return <IconButton {...props} />;
}
export default function PluginInfoActionSheet({
plugin,
navigation,
}: PluginInfoActionSheetProps) {
plugin.usePluginState();
const [loading, setLoading] = useState(false);
// Determine plugin type
const isVendettaPlugin = plugin.id.includes("/");
const isCorePlugin =
plugin.id.startsWith("bunny.") || plugin.id.startsWith("vendetta.");
const copyPluginUrl = () => {
let url = plugin.id;
if (isVendettaPlugin) {
url = plugin.id;
} else {
try {
const pluginAny = plugin;
const repoUrl =
pluginAny._manifest?.parentRepository ||
pluginAny.manifest?.parentRepository;
url = repoUrl ? `${repoUrl}/builds/${plugin.id}` : plugin.id;
} catch (e) {
url = plugin.id;
}
}
clipboard.setString(url);
showToast("Copied to clipboard!", findAssetId("toast_copy_link"));
};
const refetchPlugin = async () => {
setLoading(true);
try {
if (isVendettaPlugin) {
const vdPlugin = VdPluginManager.plugins[plugin.id];
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
await VdPluginManager.fetchPlugin(plugin.id);
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
showToast("Plugin refreshed successfully");
} else {
// For Bunny plugins
// If you have a refreshPlugin function, call it here
showToast("Plugin refreshed successfully");
}
} catch (e) {
showToast("Failed to refresh plugin");
} finally {
setLoading(false);
}
};
const clearPluginData = () => {
showConfirmationAlert({
title: "Clear Data",
content:
"Are you sure you want to clear all data for this plugin? This action cannot be undone.",
confirmText: "Clear",
confirmColor: "red",
cancelText: "Cancel",
onConfirm: async () => {
hideSheet("PluginInfoActionSheet");
onPress?.();
};
try {
if (isVendettaPlugin) {
const vdPlugin = VdPluginManager.plugins[plugin.id];
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
await purgeVdStorage(plugin.id);
if (vdPlugin.enabled) await VdPluginManager.startPlugin(plugin.id);
} else {
await purgeStorage(`plugins/storage/${plugin.id}.json`);
}
showToast("Plugin data cleared successfully");
} catch (e) {
showToast("Failed to clear plugin data");
}
},
});
};
return <IconButton {...props} />;
}
export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfoActionSheetProps) {
plugin.usePluginState();
return <ActionSheet>
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, paddingVertical: 24, justifyContent: "space-between", width: "100%" }}>
<TitleComponent plugin={plugin} />
<ContextMenu
items={[
{
label: "Details",
iconSource: findAssetId("CircleInformationIcon-primary"),
action: () => {
}
},
// {
// label: true ? "Disable Updates" : "Enable Updates",
// iconSource: true ? findAssetId("ClockXIcon") : findAssetId("ClockIcon"),
// action: () => {
// }
// },
{
label: "Clear Data",
iconSource: findAssetId("FileIcon"),
variant: "destructive",
action: () => {
}
},
{
label: "Uninstall",
iconSource: findAssetId("TrashIcon"),
variant: "destructive",
action: () => {
}
}
]}
>
{props => <IconButton
{...props}
icon={findAssetId("MoreHorizontalIcon")}
variant="secondary"
size="sm"
/>}
</ContextMenu>
</View>
<View style={{ flexDirection: "row", justifyContent: "space-around", alignContent: "center" }}>
<PluginInfoIconButton
label="Configure"
variant="secondary"
disabled={!plugin.getPluginSettingsComponent()}
icon={findAssetId("WrenchIcon")}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
});
}}
/>
<PluginInfoIconButton
label="Refetch"
variant="secondary"
icon={findAssetId("RetryIcon")}
onPress={() => {
startPlugin(plugin.id);
}}
/>
<PluginInfoIconButton
label="Copy URL"
variant="secondary"
icon={findAssetId("LinkIcon")}
onPress={() => {
}}
/>
</View>
<Card>
<Text variant="text-md/semibold" color="text-primary" style={{ marginBottom: 4 }}>
Description
</Text>
<Text variant="text-md/medium">
{plugin.description}
</Text>
</Card>
</ScrollView>
</ActionSheet>;
const uninstallPluginHandler = () => {
if (isCorePlugin) {
showToast("Core plugins cannot be uninstalled");
return;
}
showConfirmationAlert({
title: "Uninstall Plugin",
content:
"Are you sure you want to uninstall this plugin? This action cannot be undone.",
confirmText: "Uninstall",
confirmColor: "red",
cancelText: "Cancel",
onConfirm: async () => {
hideSheet("PluginInfoActionSheet");
try {
if (isVendettaPlugin) {
await VdPluginManager.removePlugin(plugin.id);
} else {
// If you have an uninstallPlugin function, call it here
}
showToast("Plugin uninstalled successfully");
} catch (e) {
showToast(
`Failed to uninstall plugin: ${e instanceof Error ? e.message : String(e)}`,
);
}
},
});
};
return (
<ActionSheet>
<ScrollView contentContainerStyle={{ gap: 12, marginBottom: 12 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingVertical: 24,
justifyContent: "space-between",
width: "100%",
}}
>
<TitleComponent plugin={plugin} />
</View>
<View
style={{
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
flexWrap: "wrap",
gap: 12,
paddingHorizontal: 4,
}}
>
<PluginInfoIconButton
label="Configure"
variant="secondary"
disabled={!plugin.getPluginSettingsComponent()}
icon={findAssetId("WrenchIcon")}
onPress={() => {
navigation.push("PUPU_CUSTOM_PAGE", {
title: plugin.name,
render: plugin.getPluginSettingsComponent(),
});
}}
/>
<PluginInfoIconButton
label="Refetch"
variant="secondary"
icon={findAssetId("RetryIcon")}
onPress={refetchPlugin}
disabled={loading}
/>
<PluginInfoIconButton
label="Copy URL"
variant="secondary"
icon={findAssetId("LinkIcon")}
onPress={copyPluginUrl}
/>
<PluginInfoIconButton
label="Clear Data"
variant="secondary"
icon={findAssetId("FileIcon")}
onPress={clearPluginData}
/>
{!isCorePlugin && (
<PluginInfoIconButton
label="Uninstall"
variant="secondary"
icon={findAssetId("TrashIcon")}
onPress={uninstallPluginHandler}
/>
)}
</View>
<Card>
<Text
variant="text-md/semibold"
color="text-primary"
style={{ marginBottom: 4 }}
>
Description
</Text>
<Text variant="text-md/medium">{plugin.description}</Text>
</Card>
</ScrollView>
</ActionSheet>
);
}