From a4eb98bd8f8a75987d39da6025be23815eb1a232 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Fri, 10 Oct 2025 23:54:06 +0200 Subject: [PATCH 1/3] Fix notrack --- src/core/plugins/notrack/index.ts | 252 ++++++++++++++++--- src/core/plugins/notrack/lib/console.ts | 16 -- src/core/plugins/notrack/lib/miscellanous.ts | 33 --- src/core/plugins/notrack/lib/network.ts | 12 - src/core/plugins/notrack/lib/sentry.ts | 26 -- src/core/plugins/notrack/lib/utils.ts | 3 - 6 files changed, 223 insertions(+), 119 deletions(-) delete mode 100644 src/core/plugins/notrack/lib/console.ts delete mode 100644 src/core/plugins/notrack/lib/miscellanous.ts delete mode 100644 src/core/plugins/notrack/lib/network.ts delete mode 100644 src/core/plugins/notrack/lib/sentry.ts delete mode 100644 src/core/plugins/notrack/lib/utils.ts diff --git a/src/core/plugins/notrack/index.ts b/src/core/plugins/notrack/index.ts index 58865de..8459303 100644 --- a/src/core/plugins/notrack/index.ts +++ b/src/core/plugins/notrack/index.ts @@ -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): 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; + + const sentrified: ConsoleFunctions = {}; + + try { + // We need to use Object.keys because TypeScript doesn't allow indexing Console with strings + (Object.keys(console) as Array).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"); + }, +}); diff --git a/src/core/plugins/notrack/lib/console.ts b/src/core/plugins/notrack/lib/console.ts deleted file mode 100644 index 7a69471..0000000 --- a/src/core/plugins/notrack/lib/console.ts +++ /dev/null @@ -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]); -} diff --git a/src/core/plugins/notrack/lib/miscellanous.ts b/src/core/plugins/notrack/lib/miscellanous.ts deleted file mode 100644 index b6a6093..0000000 --- a/src/core/plugins/notrack/lib/miscellanous.ts +++ /dev/null @@ -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()); -} diff --git a/src/core/plugins/notrack/lib/network.ts b/src/core/plugins/notrack/lib/network.ts deleted file mode 100644 index 68f762e..0000000 --- a/src/core/plugins/notrack/lib/network.ts +++ /dev/null @@ -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) }, - ); -} diff --git a/src/core/plugins/notrack/lib/sentry.ts b/src/core/plugins/notrack/lib/sentry.ts deleted file mode 100644 index 5b01b7e..0000000 --- a/src/core/plugins/notrack/lib/sentry.ts +++ /dev/null @@ -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(); - }; -} diff --git a/src/core/plugins/notrack/lib/utils.ts b/src/core/plugins/notrack/lib/utils.ts deleted file mode 100644 index 55f871f..0000000 --- a/src/core/plugins/notrack/lib/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { instead } from "@lib/api/patcher"; - -export const noop = (prop: string, parent: any) => instead(prop, parent, () => {}); From beb52729f59de9251e2bc49b7551eff7bd33fe83 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 11 Oct 2025 10:24:19 +0200 Subject: [PATCH 2/3] port messagefix from ShiggyCord --- src/core/plugins/index.ts | 21 ++++++----- src/core/plugins/messagefix/index.ts | 56 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 src/core/plugins/messagefix/index.ts diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 7b423f7..5bc2f76 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -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 => ({ - "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; } diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts new file mode 100644 index 0000000..146bf2b --- /dev/null +++ b/src/core/plugins/messagefix/index.ts @@ -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"); + }, +}); From 93d5c354a119be52a3a8d99bcc49860c737e6b3f Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sat, 11 Oct 2025 11:38:51 +0200 Subject: [PATCH 3/3] port information menu look from ShiggyCord (was exclusive only for core plugins) --- .../pages/Plugins/components/PluginCard.tsx | 256 ++++++++------- src/core/ui/settings/pages/Plugins/index.tsx | 293 +++++++++++------ .../ui/settings/pages/Plugins/models/bunny.ts | 79 +++-- .../settings/pages/Plugins/models/vendetta.ts | 60 ++-- .../Plugins/sheets/PluginInfoActionSheet.tsx | 303 ++++++++++++------ 5 files changed, 616 insertions(+), 375 deletions(-) diff --git a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx index b199f55..6ab8f1e 100644 --- a/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx +++ b/src/core/ui/settings/pages/Plugins/components/PluginCard.tsx @@ -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; }>(null!); +const CardContext = createContext<{ + plugin: UnifiedPluginModel; + result: Fuzzysort.KeysResult; +}>(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) => - - {m} - - ); + // could be empty if the plugin name is irrelevant! + const highlightedNode = result[0].highlight((m, i) => ( + + {m} + + )); - const icon = plugin.icon && findAssetId(plugin.icon); + const icon = plugin.icon && findAssetId(plugin.icon); - const textNode = ( - - {highlightedNode.length ? highlightedNode : plugin.name} - - ); + const textNode = ( + + {highlightedNode.length ? highlightedNode : plugin.name} + + ); - return - {icon && } - {textNode} - ; + return ( + + {icon && } + {textNode} + + ); } 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) => - - {m} - - ); + // could be empty if the author(s) are irrelevant with the search! + const highlightedNode = result[2].highlight((m, i) => ( + + {m} + + )); - 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 ( - - - by {authorText} - - {badges.length > 0 && - {badges.map((b, i) => )} - } + return ( + + + by {authorText} + + {badges.length > 0 && ( + + {badges.map((b, i) => ( + + ))} - ); + )} + + ); } 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) => - {m} - ); + // could be empty if the description is irrelevant with the search! + const highlightedNode = result[1].highlight((m, i) => ( + + {m} + + )); - return - {highlightedNode.length ? highlightedNode : plugin.description} - ; + return ( + + {highlightedNode.length ? highlightedNode : plugin.description} + + ); } const Actions = () => { - const { plugin } = useCardContext(); - const navigation = NavigationNative.useNavigation(); + const { plugin } = useCardContext(); + const navigation = NavigationNative.useNavigation(); - return - navigation.push("PUPU_CUSTOM_PAGE", { - title: plugin.name, - render: plugin.getPluginSettingsComponent(), - })} - /> - void showSheet( - "PluginInfoActionSheet", - plugin.resolveSheetComponent(), - { plugin, navigation } - )} - /> - ; + return ( + + + navigation.push("PUPU_CUSTOM_PAGE", { + title: plugin.name, + render: plugin.getPluginSettingsComponent(), + }) + } + /> + + void showSheet( + "PluginInfoActionSheet", + plugin.resolveSheetComponent(), + { plugin, navigation }, + ) + } + /> + + ); }; -export default function PluginCard({ result, item: plugin }: CardWrapper) { - plugin.usePluginState(); +export default function PluginCard({ + result, + item: plugin, +}: CardWrapper) { + 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 ( - - - - - - - <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> + ); } diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 9c5405c..5f665b7 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -3,7 +3,12 @@ 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 { + 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"; @@ -13,7 +18,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"; @@ -21,116 +32,198 @@ 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 vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin); - const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin); + const vdPlugins = Object.values(VdPluginManager.plugins).map( + unifyVdPlugin, + ); + const bnPlugins = [...registeredPlugins.values()] + .filter( + (p) => + p && + typeof p.id === "string" && + 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; + 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; - 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); + } + }, + }} + /> + ); } diff --git a/src/core/ui/settings/pages/Plugins/models/bunny.ts b/src/core/ui/settings/pages/Plugins/models/bunny.ts index 106be65..252e6ff 100644 --- a/src/core/ui/settings/pages/Plugins/models/bunny.ts +++ b/src/core/ui/settings/pages/Plugins/models/bunny.ts @@ -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); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/models/vendetta.ts b/src/core/ui/settings/pages/Plugins/models/vendetta.ts index ee735f5..fc36653 100644 --- a/src/core/ui/settings/pages/Plugins/models/vendetta.ts +++ b/src/core/ui/settings/pages/Plugins/models/vendetta.ts @@ -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); + }, + }; } diff --git a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx index 4c769df..e105640 100644 --- a/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx +++ b/src/core/ui/settings/pages/Plugins/sheets/PluginInfoActionSheet.tsx @@ -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> + ); }