From 6b7ec921498e76e540956772f4928d8ca43eedf1 Mon Sep 17 00:00:00 2001 From: kmmiio99o <164557248+kmmiio99o@users.noreply.github.com> Date: Sun, 12 Oct 2025 17:46:51 +0200 Subject: [PATCH] Add Message Logger and ShiggyHelper core plugins, improve plugin listing and settings UI - Added Message Logger core plugin (port of Antied) - Added ShiggyHelper core plugin with verbose logging and log dump UI - Improved /plugins command to show core and external plugins separately - Split core and external plugins into separate settings pages - Updated theme selection logic for better color sync - Updated MessageFix author attribution - Bumped version to 1.1.0 --- scripts/build.mjs | 2 +- src/core/commands/plugins.ts | 218 ++++++++++++++--- src/core/plugins/badges/index.tsx | 156 ++++++------ src/core/plugins/index.ts | 2 + src/core/plugins/messagefix/index.ts | 2 +- src/core/plugins/messagelogger/index.jsx | 43 ++++ .../messagelogger/patches/actionsheet.jsx | 139 +++++++++++ .../messagelogger/patches/flux_dispatch.js | 125 ++++++++++ .../messagelogger/patches/self_edit.js | 21 ++ src/core/plugins/shiggyhelper/index.tsx | 226 ++++++++++++++++++ src/core/ui/settings/index.ts | 11 +- .../ui/settings/pages/Plugins/CorePlugins.tsx | 40 ++++ src/core/ui/settings/pages/Plugins/index.tsx | 3 +- src/lib/addons/themes/colors/updater.ts | 11 +- src/lib/addons/themes/index.ts | 4 +- src/modules.d.ts | 2 +- 16 files changed, 889 insertions(+), 116 deletions(-) create mode 100644 src/core/plugins/messagelogger/index.jsx create mode 100644 src/core/plugins/messagelogger/patches/actionsheet.jsx create mode 100644 src/core/plugins/messagelogger/patches/flux_dispatch.js create mode 100644 src/core/plugins/messagelogger/patches/self_edit.js create mode 100644 src/core/plugins/shiggyhelper/index.tsx create mode 100644 src/core/ui/settings/pages/Plugins/CorePlugins.tsx diff --git a/scripts/build.mjs b/scripts/build.mjs index 66143d3..9e8c823 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -74,7 +74,7 @@ const config = { constModules: { globals: { "bunny-build-info": { - version: `"1.1.0-3"`, + version: `"1.1.0"`, }, }, }, diff --git a/src/core/commands/plugins.ts b/src/core/commands/plugins.ts index 8933713..b83f6a7 100644 --- a/src/core/commands/plugins.ts +++ b/src/core/commands/plugins.ts @@ -1,43 +1,199 @@ import { Strings } from "@core/i18n"; -import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins"; -import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types"; +import { + ApplicationCommand, + ApplicationCommandOptionType, +} from "@lib/api/commands/types"; import { messageUtil } from "@metro/common"; +import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins"; +import { + registeredPlugins, + corePluginInstances, + pluginSettings, +} from "@lib/addons/plugins"; -export default () => { +/** + * /plugins + * + * Aggregates: + * - Core plugins (built-in) from corePluginInstances + * - External plugins: + * - Vendetta-managed plugins (VdPluginManager.plugins) + * - Bunny repository plugins (registeredPlugins + pluginSettings) + * + * Dedupes by plugin id and shows Enabled / Disabled groups. + * For external plugins each entry will show its source(s) (vendetta / bunny). + */ +export default () => + { name: "plugins", description: Strings.COMMAND_PLUGINS_DESC, options: [ - { - name: "ephemeral", - displayName: "ephemeral", - type: ApplicationCommandOptionType.BOOLEAN, - description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, - } + { + name: "ephemeral", + displayName: "ephemeral", + type: ApplicationCommandOptionType.BOOLEAN, + description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, + }, ], execute([ephemeral], ctx) { - const plugins = Object.values(VdPluginManager.plugins).filter(Boolean) as unknown as VendettaPlugin[]; - plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); + // --- Core plugins --- + const coreIds = [...corePluginInstances.keys()]; - const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name); - const disabled = plugins.filter(p => !p.enabled).map(p => p.manifest.name); + const coreManifests = coreIds + .map((id) => registeredPlugins.get(id)) + .filter(Boolean) as any[]; - const content = [ - `**Installed Plugins (${plugins.length}):**`, - ...(enabled.length > 0 ? [ - `Enabled (${enabled.length}):`, - "> " + enabled.join(", "), - ] : []), - ...(disabled.length > 0 ? [ - `Disabled (${disabled.length}):`, - "> " + disabled.join(", "), - ] : []), - ].join("\n"); + const coreEnabled = coreManifests + .filter((m) => Boolean(pluginSettings[m.id]?.enabled)) + .map((m) => m.display?.name ?? m.name ?? m.id) + .sort((a, b) => a.localeCompare(b)); + const coreDisabled = coreManifests + .filter((m) => !Boolean(pluginSettings[m.id]?.enabled)) + .map((m) => m.display?.name ?? m.name ?? m.id) + .sort((a, b) => a.localeCompare(b)); - if (ephemeral?.value) { - messageUtil.sendBotMessage(ctx.channel.id, content); - } else { - const fixNonce = Date.now().toString(); - messageUtil.sendMessage(ctx.channel.id, { content }, void 0, {nonce:fixNonce}); + // --- External plugins aggregation (Vendetta + Bunny) --- + type ExtEntry = { + id: string; + name: string; + sources: Set; + enabled: boolean; // overall enabled if any source reports enabled (Vendetta) or pluginSettings for Bunny + }; + + const aggregated = new Map(); + + // Vendetta plugins (if available) + try { + const vdRaw = + (VdPluginManager && (VdPluginManager as any).plugins) || {}; + const vdPlugins = Object.values(vdRaw).filter( + Boolean, + ) as VendettaPlugin[]; + + for (const p of vdPlugins) { + const id = + p.manifest?.id ?? p.id ?? String(p.manifest?.name ?? Math.random()); + const name = p.manifest?.name ?? id; + const enabled = Boolean(p.enabled); + const existing = aggregated.get(id); + if (existing) { + existing.sources.add("vendetta"); + existing.enabled = existing.enabled || enabled; + } else { + aggregated.set(id, { + id, + name, + sources: new Set(["vendetta"]), + enabled, + }); + } } - } -}; + } catch (e) { + // Defensive: if Vendetta manager isn't ready, skip vendetta aggregation + } + + // Bunny external plugins: use registeredPlugins + pluginSettings (installed) + for (const [id, manifest] of registeredPlugins) { + if (!manifest) continue; + // External bunny plugins have parentRepository property + // Only consider those that are actual external plugins (have parentRepository) + const isExternal = (manifest as any).parentRepository != null; + if (!isExternal) continue; + + // Skip core plugin ids (safety) + if (corePluginInstances.has(id)) continue; + + const name = manifest.display?.name ?? manifest.name ?? id; + const installed = pluginSettings[id] != null; + const enabled = Boolean(pluginSettings[id]?.enabled); + + // If not installed and not present in aggregated (vendetta), still include as available but mark enabled=false + const existing = aggregated.get(id); + if (existing) { + existing.sources.add("bunny"); + existing.enabled = existing.enabled || enabled; + } else { + // Only add bunny entry if installed OR if it's registered and not installed we will handle as available later + if (installed) { + aggregated.set(id, { + id, + name, + sources: new Set(["bunny"]), + enabled, + }); + } + } + } + + // Also collect available-but-not-installed Bunny externals for the "available" list + const available = [...registeredPlugins.values()] + .filter( + (m: any) => + Boolean(m) && + (m as any).parentRepository && + !corePluginInstances.has((m as any).id), + ) + .map((m: any) => ({ + id: m.id, + name: m.display?.name ?? m.name ?? m.id, + installed: pluginSettings[m.id] != null, + })) + .filter((x) => !x.installed) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((x) => x.name); + + // Build external lists (deduped) + const externalList = Array.from(aggregated.values()).sort((a, b) => + a.name.localeCompare(b.name), + ); + + const externalEnabled = externalList + .filter((e) => e.enabled) + .map((e) => e.name); + const externalDisabled = externalList + .filter((e) => !e.enabled) + .map((e) => e.name); + + // Build message + const lines: string[] = []; + + // Core plugins + lines.push(`**Plugins (${coreManifests.length}):**`); + if (coreEnabled.length > 0) { + lines.push(`Enabled (${coreEnabled.length}):`); + lines.push("> " + coreEnabled.join(", ")); + } + if (coreDisabled.length > 0) { + lines.push(`Disabled (${coreDisabled.length}):`); + lines.push("> " + coreDisabled.join(", ")); + } + if (coreManifests.length === 0) { + lines.push("_No core plugins registered._"); + } + + // External plugins + lines.push(`**External Plugins (${externalList.length}):**`); + if (externalEnabled.length > 0) { + lines.push(`Installed & Enabled (${externalEnabled.length}):`); + lines.push("> " + externalEnabled.join(", ")); + } + if (externalDisabled.length > 0) { + lines.push(`Installed & Disabled (${externalDisabled.length}):`); + lines.push("> " + externalDisabled.join(", ")); + } + if (externalList.length === 0) { + lines.push("_No external plugins found._"); + } + + const content = lines.join("\n"); + + if (ephemeral?.value) { + messageUtil.sendBotMessage(ctx.channel.id, content); + } else { + const fixNonce = Date.now().toString(); + messageUtil.sendMessage(ctx.channel.id, { content }, void 0, { + nonce: fixNonce, + }); + } + }, + }; diff --git a/src/core/plugins/badges/index.tsx b/src/core/plugins/badges/index.tsx index 79baa15..3cca7f2 100644 --- a/src/core/plugins/badges/index.tsx +++ b/src/core/plugins/badges/index.tsx @@ -5,88 +5,90 @@ import { useEffect, useState } from "react"; import { defineCorePlugin } from ".."; interface BunnyBadge { - label: string; - url: string; + label: string; + url: string; } const useBadgesModule = findByName("useBadges", false); export default defineCorePlugin({ - manifest: { - id: "bunny.badges", - version: "1.1.0", - type: "plugin", - spec: 3, - main: "", - display: { - name: "Badges", - description: "Adds badges to user's profile", - authors: [{ name: "cocobo1"}, { name: "pylixonly" }] - } + manifest: { + id: "bunny.badges", + version: "1.1.0", + type: "plugin", + spec: 3, + main: "", + display: { + name: "Badges", + description: "Adds badges to user's profile", + authors: [{ name: "cocobo1" }, { name: "pylixonly" }], }, - - start() { - let allBadges: { [x: string]: any; } | null = null; - const badgeProps = {} as Record; - - onJsxCreate("ProfileBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; - if (cachedProps) { - ret.props.source = cachedProps.source; - ret.props.label = cachedProps.label; - ret.props.id = cachedProps.id; - } - } - }); - - onJsxCreate("RenderedBadge", (component, ret) => { - if (ret.props.id?.startsWith("bunny-")) { - const cachedProps = badgeProps[ret.props.id]; - if (cachedProps) { - Object.assign(ret.props, cachedProps); - } - } - }); - - after("default", useBadgesModule, ([user], result) => { - const [badges, setBadges] = useState([]); - - useEffect(() => { - if (!user) return; - - if (!allBadges) { - fetch("https://codeberg.org/raincord/badges/raw/branch/main/badges.json") - .then(r => r.json()) - .then(data => { - allBadges = data; - //@ts-expect-error - setBadges(allBadges[user.userId] || []); - }) - } else { - //user has no badges, but maybe they should get some by contributing - setBadges(allBadges[user.userId] || []); - } - }, [user?.userId]); - - if (user && badges.length > 0) { - badges.forEach((badge, i) => { - const badgeId = `bunny-${user.userId}-${i}`; - - badgeProps[badgeId] = { - id: badgeId, - source: { uri: badge.url }, - label: badge.label, - userId: user.userId, - }; + }, - result.push({ - id: badgeId, - description: badge.label, - icon: " _", - }); - }); - } + start() { + let allBadges: { [x: string]: any } | null = null; + const badgeProps = {} as Record; + + onJsxCreate("ProfileBadge", (component, ret) => { + if (ret.props.id?.startsWith("bunny-")) { + const cachedProps = badgeProps[ret.props.id]; + if (cachedProps) { + ret.props.source = cachedProps.source; + ret.props.label = cachedProps.label; + ret.props.id = cachedProps.id; + } + } + }); + + onJsxCreate("RenderedBadge", (component, ret) => { + if (ret.props.id?.startsWith("bunny-")) { + const cachedProps = badgeProps[ret.props.id]; + if (cachedProps) { + Object.assign(ret.props, cachedProps); + } + } + }); + + after("default", useBadgesModule, ([user], result) => { + const [badges, setBadges] = useState([]); + + useEffect(() => { + if (!user) return; + + if (!allBadges) { + fetch( + "https://codeberg.org/raincord/badges/raw/branch/main/badges.json", + ) + .then((r) => r.json()) + .then((data) => { + allBadges = data; + //@ts-expect-error + setBadges(allBadges[user.userId] || []); + }); + } else { + //user has no badges, but maybe they should get some by contributing + setBadges(allBadges[user.userId] || []); + } + }, [user?.userId]); + + if (user && badges.length > 0) { + badges.forEach((badge, i) => { + const badgeId = `bunny-${user.userId}-${i}`; + + badgeProps[badgeId] = { + id: badgeId, + source: { uri: badge.url }, + label: badge.label, + userId: user.userId, + }; + + result.push({ + id: badgeId, + description: badge.label, + icon: " _", + }); }); - } -}); \ No newline at end of file + } + }); + }, +}); diff --git a/src/core/plugins/index.ts b/src/core/plugins/index.ts index 6657185..8445ecf 100644 --- a/src/core/plugins/index.ts +++ b/src/core/plugins/index.ts @@ -11,7 +11,9 @@ export const getCorePlugins = (): Record => ({ "bunny.badges": require("./badges"), "bunny.notrack": { ...require("./notrack"), preenabled: true }, "bunny.messagefix": { ...require("./messagefix") }, + "bunny.messagelogger": { ...require("./messagelogger"), preenabled: true }, "bunny.fixembed": { ...require("./fixembed"), preenabled: true }, + "bunny.shiggyhelper": { ...require("./shiggyhelper") }, }); /** diff --git a/src/core/plugins/messagefix/index.ts b/src/core/plugins/messagefix/index.ts index 146bf2b..15731a8 100644 --- a/src/core/plugins/messagefix/index.ts +++ b/src/core/plugins/messagefix/index.ts @@ -16,7 +16,7 @@ export default defineCorePlugin({ display: { name: "MessageFix", description: "Ensures messages include the required nonce parameter", - authors: [{ name: "ShiggyCord Team" }], + authors: [{ name: "Win8.1VMUser (plugin by ShiggyCord Team)" }], }, }, diff --git a/src/core/plugins/messagelogger/index.jsx b/src/core/plugins/messagelogger/index.jsx new file mode 100644 index 0000000..1da1e16 --- /dev/null +++ b/src/core/plugins/messagelogger/index.jsx @@ -0,0 +1,43 @@ +import { defineCorePlugin } from ".."; +import fluxDispatchPatch from "./patches/flux_dispatch"; +import selfEditPatch from "./patches/self_edit"; +import actionsheet from "./patches/actionsheet"; + +export const regexEscaper = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +export let isEnabled = false; + +const deletedMessageArray = new Map(); +let patches = []; + +export default defineCorePlugin({ + manifest: { + id: "bunny.messagelogger", + version: "1.0.0", + type: "plugin", + spec: 3, + main: "", + display: { + name: "Message Logger", + description: "Port of the popular Revenge/Kettu plugin called Antied.", + authors: [{ name: "ShiggyCord Team, Angelw0lf" }], + }, + }, + + start() { + isEnabled = true; + + // Each patch function is expected to return an unpatch function. + patches = [ + fluxDispatchPatch(deletedMessageArray), + actionsheet(), + selfEditPatch(), + ].filter(Boolean); + }, + + stop() { + isEnabled = false; + + patches.forEach((p) => p && p()); + patches = []; + }, +}); diff --git a/src/core/plugins/messagelogger/patches/actionsheet.jsx b/src/core/plugins/messagelogger/patches/actionsheet.jsx new file mode 100644 index 0000000..3395b62 --- /dev/null +++ b/src/core/plugins/messagelogger/patches/actionsheet.jsx @@ -0,0 +1,139 @@ +import { before, after } from "@lib/api/patcher"; +import { findAssetId } from "@lib/api/assets"; +import { findInReactTree } from "@lib/utils"; +import { FluxDispatcher, React } from "@metro/common"; +import { showToast } from "@lib/ui/toasts"; +import { findByProps } from "@metro"; +import { regexEscaper, isEnabled } from ".."; + +const ActionSheet = findByProps("openLazy", "hideActionSheet"); +const MessageStore = findByProps("getMessage", "getMessages"); +const ChannelStore = findByProps("getChannel", "getDMFromUserId"); +const ChannelMessages = findByProps("_channelMessages"); +const { ActionSheetRow } = findByProps("ActionSheetRow"); + +/** @param {any} a */ +function someFunc(a) { + // return a?.props?.label == i18n?.Messages?.MESSAGE_ACTION_REPLY + return a?.props?.label?.toLowerCase?.() == "reply"; +} + +export default () => + before("openLazy", ActionSheet, ([component, args, actionMessage]) => { + if (isEnabled) { + try { + const message = actionMessage?.message; + + if (args !== "MessageLongPressActionSheet" || !message) return; + + component.then((/** @type {any} */ instance) => { + const unpatch = after("default", instance, (_, comp) => { + try { + React.useEffect( + () => () => { + unpatch(); + }, + [], + ); + + const buttons = findInReactTree(comp, (c) => c?.find?.(someFunc)); + if (!buttons) return comp; + + const position = Math.max( + buttons.findIndex(someFunc), + buttons.length - 1, + ); + + let originalMessage = null; + + if (message?.channel_id && message?.id) { + originalMessage = MessageStore.getMessage( + message?.channel_id, + message?.id, + ); + + if (!originalMessage) { + const channel = ChannelMessages.get(message?.channel_id); + originalMessage = channel?.get(message?.id); + } + } + + if (!originalMessage) return comp; + + const escapedBuffer = regexEscaper("`[ EDITED ]`\n\n"); + + const separator = new RegExp(escapedBuffer, "gmi"); + const checkIfBufferExist = separator.test(message.content); + + if (checkIfBufferExist) { + const targetPos = position || 1; + + const actionRow = React.createElement( + ActionSheetRow, + { + label: "Remove Edit History", + subLabel: "Added by Message Logger", + icon: React.createElement(ActionSheetRow.Icon, { + source: findAssetId("ic_edit_24px"), + }), + onPress: () => { + try { + const lats = message?.content?.split(separator); + const targetMessage = lats[lats.length - 1]; + + FluxDispatcher.dispatch({ + type: "MESSAGE_UPDATE", + message: { + ...message, + message_reference: + message?.message_reference || + message?.messageReference || + null, + content: `${targetMessage}`, + guild_id: ChannelStore.getChannel( + originalMessage.channel_id, + ).guild_id, + }, + otherPluginBypass: true, + }); + + ActionSheet.hideActionSheet(); + showToast( + "History Removed", + findAssetId("ic_edit_24px"), + ); + } catch (ex) { + showToast( + "[Message Logger] Failed to remove edit history", + ); + console.error( + "[Message Logger] Error while removing history", + ex, + ); + } + }, + }, + null, + ); + + buttons.splice(targetPos, 0, actionRow); + } + } catch (e) { + showToast( + "[Message Logger] Crash on ActionSheet, check debug log for more info", + ); + console.error( + "[Message Logger] Error > ActionSheet:Component Patch\n", + e, + ); + } + }); + }); + } catch (e) { + showToast( + "[Message Logger] Crash on ActionSheet, check debug log for more info", + ); + console.error("[Message Logger] Error > ActionSheet Patch\n", e); + } + } + }); diff --git a/src/core/plugins/messagelogger/patches/flux_dispatch.js b/src/core/plugins/messagelogger/patches/flux_dispatch.js new file mode 100644 index 0000000..8c28cec --- /dev/null +++ b/src/core/plugins/messagelogger/patches/flux_dispatch.js @@ -0,0 +1,125 @@ +import { before } from "@lib/api/patcher"; +import { findByProps } from "@metro"; +import { showToast } from "@lib/ui/toasts"; +import { FluxDispatcher } from "@metro/common"; + +import { isEnabled } from ".."; + +const ChannelStore = findByProps("getChannel", "getDMFromUserId"); +const ChannelMessages = findByProps("_channelMessages"); +const MessageStore = findByProps("getMessage", "getMessages"); + +export default (deletedMessageArray) => + before("dispatch", FluxDispatcher, (args) => { + if (isEnabled) { + try { + const ev = args[0]; + if (!ev || !ev.type) return; + + /* ========================================================= + MESSAGE_DELETE + ==========================================================*/ + if (ev.type === "MESSAGE_DELETE") { + if (ev.otherPluginBypass) return; + + const orig = ChannelMessages.get(ev.channelId)?.get(ev.id); + if (!orig?.author?.id || !orig.author.username) return; + + // ephemeral message dismiss (from bots) + if ((orig?.author?.bot && orig?.flags == 64) || orig.author.bot) + return; + + // empty message check + if ( + !orig.content && + !orig.attachments?.length && + !orig.embeds?.length + ) + return; + + const entry = deletedMessageArray.get(ev.id); + if (entry?.stage === 2) { + if (deletedMessageArray.size >= 100) { + deletedMessageArray = new Map(); + } + return; // Kill message normally + } + if (entry?.stage === 1) { + // first pass + entry.stage = 2; + return entry.message || args; + } + + const guildId = ChannelStore.getChannel( + orig.channel_id || ev.channelId, + )?.guild_id; + + /* reuse the same object shape every time */ + + ev.message = { + ...orig, + content: orig.content, + channel_id: orig.channel_id || ev.channelId, + guild_id: guildId, + message_reference: + orig?.message_reference || orig?.messageReference || null, + flags: 64, + }; + + ev.type = "MESSAGE_UPDATE"; + ev.channelId = orig.channel_id || ev.channelId; + ev.optimistic = false; + ev.sendMessageOptions = {}; + ev.isPushNotification = false; + + deletedMessageArray.set(ev.id, { message: args, stage: 1 }); + + return args; + } + + /* ========================================================= + MESSAGE_UPDATE + ==========================================================*/ + if (ev.type === "MESSAGE_UPDATE") { + if (ev.otherPluginBypass) return; + const msg = ev.message; + + if (!msg || msg.author?.bot) return; + + const chId = msg.channel_id || ev.channelId; + const id = msg.id || ev.id; + + const orig = + MessageStore.getMessage(chId, id) || + ChannelMessages.get(chId)?.get(id); + + if (!orig?.author?.id || !orig.author.username) return; + + if ( + !orig.content && + !orig.attachments?.length && + !orig.embeds?.length + ) + return; + + if (!msg.content || msg.content === orig.content) return; + + let prefix = "`[ EDITED ]`\n\n"; + + ev.message = { + ...msg, + content: `${orig.content} ${prefix}${msg.content}`, + guild_id: ChannelStore.getChannel(chId)?.guild_id ?? msg.guild_id, + edited_timestamp: "invalid_timestamp", + message_reference: + msg?.message_reference || orig?.messageReference || null, + }; + + return args; + } + } catch (e) { + showToast("[Message Logger] FluxDispatcher crash – check logs"); + console.error("[Message Logger] Flux patch\n", e); + } + } + }); diff --git a/src/core/plugins/messagelogger/patches/self_edit.js b/src/core/plugins/messagelogger/patches/self_edit.js new file mode 100644 index 0000000..93e399d --- /dev/null +++ b/src/core/plugins/messagelogger/patches/self_edit.js @@ -0,0 +1,21 @@ +import { before } from "@lib/api/patcher"; +import { findByProps } from "@metro"; +import { regexEscaper, isEnabled } from ".."; + +const Message = findByProps("sendMessage", "startEditMessage"); + +export default () => + before("startEditMessage", Message, (args) => { + if (!isEnabled) return; + + const escaped = regexEscaper("`[ EDITED ]`\n\n"); + const regexPattern = new RegExp(escaped, "gmi"); + + const [, , msg] = args; + if (typeof msg !== "string") return; + + const parts = msg.split(regexPattern); + const final = parts[parts.length - 1] ?? ""; + + args[2] = final; + }); diff --git a/src/core/plugins/shiggyhelper/index.tsx b/src/core/plugins/shiggyhelper/index.tsx new file mode 100644 index 0000000..f9fbe13 --- /dev/null +++ b/src/core/plugins/shiggyhelper/index.tsx @@ -0,0 +1,226 @@ +import { defineCorePlugin } from ".."; +import { findByProps } from "@metro"; +import { logger } from "@lib/utils/logger"; +import { settings } from "@lib/api/settings"; +import { React } from "@metro/common"; +import { showToast } from "@lib/ui/toasts"; +import { ScrollView } from "react-native"; + +// Button imported from components above + +declare module "@lib/api/settings" { + interface Settings { + shiggyhelper?: { + verboseLogging?: boolean; + }; + } +} + +let originalConsoleLog: typeof console.log | null = null; +let originalConsoleError: typeof console.error | null = null; +let isPatched = false; +const capturedLogs: string[] = []; + +function applyVerbosePatch(enabled: boolean) { + if (enabled && !isPatched) { + originalConsoleLog = console.log; + originalConsoleError = console.error; + + console.log = (...args: any[]) => { + try { + const msg = args + .map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + .join(" "); + capturedLogs.push(`[LOG ${new Date().toISOString()}] ${msg}`); + } catch { + // ignore stringify errors + capturedLogs.push(`[LOG ${new Date().toISOString()}] (unserializable)`); + } + originalConsoleLog?.apply(console, args); + }; + + console.error = (...args: any[]) => { + try { + const msg = args + .map((a) => (typeof a === "string" ? a : JSON.stringify(a))) + .join(" "); + capturedLogs.push(`[ERROR ${new Date().toISOString()}] ${msg}`); + } catch { + capturedLogs.push( + `[ERROR ${new Date().toISOString()}] (unserializable)`, + ); + } + originalConsoleError?.apply(console, args); + }; + + isPatched = true; + logger.log("ShiggyHelper: Verbose logging enabled"); + } else if (!enabled && isPatched) { + if (originalConsoleLog) console.log = originalConsoleLog; + if (originalConsoleError) console.error = originalConsoleError; + originalConsoleLog = null; + originalConsoleError = null; + isPatched = false; + logger.log("ShiggyHelper: Verbose logging disabled"); + } +} + +export default defineCorePlugin({ + manifest: { + id: "bunny.shiggyhelper", + version: "1.0.0", + type: "plugin", + spec: 3, + main: "", + display: { + name: "ShiggyHelper", + description: + "Just a ShiggyHelper, because why not if you can have good logging?", + authors: [{ name: "ShiggyCord Team" }], + }, + }, + + /** + * Simple settings UI exposing a toggle and some buttons for helper actions. + * Uses the project's Forms components via findByProps. + */ + SettingsComponent() { + const { useState, useEffect } = React; + + // Use table-style rows (TableRowGroup / TableSwitchRow) for settings UI. + // Resolve UI row components; guard if unavailable. + const { TableRow, TableRowGroup, TableSwitchRow } = findByProps( + "TableRow", + "TableRowGroup", + "TableSwitchRow", + ); + + if (!TableRow || !TableRowGroup || !TableSwitchRow) { + return React.createElement( + ScrollView, + { style: { flex: 1, padding: 12 } }, + React.createElement( + Text, + null, + "ShiggyHelper UI unavailable (missing TableRow components).", + ), + ); + } + + const [verbose, setVerbose] = useState( + settings.shiggyhelper?.verboseLogging ?? false, + ); + + useEffect(() => { + // persist and apply + settings.shiggyhelper = settings.shiggyhelper || {}; + settings.shiggyhelper.verboseLogging = verbose; + applyVerbosePatch(Boolean(verbose)); + }, [verbose]); + + const dumpLogs = () => { + if (!capturedLogs.length) { + showToast("No logs captured"); + return; + } + const toShow = capturedLogs.slice(-10).join("\n"); + logger.log("ShiggyHelper: Log dump (last 10):\n" + toShow); + showToast("Dumped last 10 logs to debug log"); + }; + + const clearLogs = () => { + capturedLogs.length = 0; + showToast("Captured logs cleared"); + }; + + const triggerTestLog = () => { + console.log("ShiggyHelper test log at", new Date().toISOString()); + console.error("ShiggyHelper test error at", new Date().toISOString()); + showToast("Test logs emitted"); + }; + + // Actions should be disabled while verbose logging is off + const actionsDisabled = !verbose; + + return React.createElement(ScrollView, { style: { flex: 1 } }, [ + React.createElement( + TableRowGroup, + { title: "ShiggyHelper" }, + React.createElement(TableSwitchRow, { + label: "Verbose Logging", + subLabel: + "Capture console.log and console.error into internal buffer", + value: verbose, + onValueChange: (v: boolean) => setVerbose(v), + icon: null, + }), + ), + + React.createElement( + TableRowGroup, + { title: "Actions" }, + React.createElement(TableRow, { + key: "dump", + label: "Dump Logs", + subLabel: "Write recent captured logs to the debug logger", + trailing: React.createElement(TableRow.TrailingText, { + text: "Dump", + }), + onPress: () => { + if (actionsDisabled) { + showToast("Enable verbose logging first"); + return; + } + dumpLogs(); + }, + disabled: actionsDisabled, + }), + React.createElement(TableRow, { + key: "clear", + label: "Clear Logs", + subLabel: "Clear the internal captured log buffer", + trailing: React.createElement(TableRow.TrailingText, { + text: "Clear", + }), + onPress: () => { + if (actionsDisabled) { + showToast("Enable verbose logging first"); + return; + } + clearLogs(); + }, + disabled: actionsDisabled, + }), + React.createElement(TableRow, { + key: "emit", + label: "Emit Test Logs", + subLabel: "Emit a test console.log / console.error entry", + trailing: React.createElement(TableRow.TrailingText, { + text: "Emit", + }), + onPress: () => { + if (actionsDisabled) { + showToast("Enable verbose logging first"); + return; + } + triggerTestLog(); + }, + disabled: actionsDisabled, + }), + ), + ]); + }, + + start() { + // Ensure settings object exists and apply patches according to stored config + settings.shiggyhelper = settings.shiggyhelper || { verboseLogging: false }; + applyVerbosePatch(Boolean(settings.shiggyhelper.verboseLogging)); + logger.log("ShiggyHelper: started"); + }, + + stop() { + // Restore console if patched + applyVerbosePatch(false); + logger.log("ShiggyHelper: stopped"); + }, +}); diff --git a/src/core/ui/settings/index.ts b/src/core/ui/settings/index.ts index 5ddc603..e7f38bc 100644 --- a/src/core/ui/settings/index.ts +++ b/src/core/ui/settings/index.ts @@ -21,9 +21,18 @@ export default function initSettings() { useTrailing: () => `(${version})`, }, { - key: "BUNNY_PLUGINS", + key: "CORE_PLUGINS", title: () => Strings.PLUGINS, icon: findAssetId("ActivitiesIcon"), + render: () => import("@core/ui/settings/pages/Plugins/CorePlugins"), + }, + { + key: "BUNNY_PLUGINS", + title: () => "External Plugins", + icon: + findAssetId("DownloadIcon") ?? + findAssetId("LinkIcon") ?? + findAssetId("ActivitiesIcon"), render: () => import("@core/ui/settings/pages/Plugins"), }, { diff --git a/src/core/ui/settings/pages/Plugins/CorePlugins.tsx b/src/core/ui/settings/pages/Plugins/CorePlugins.tsx new file mode 100644 index 0000000..d36c1da --- /dev/null +++ b/src/core/ui/settings/pages/Plugins/CorePlugins.tsx @@ -0,0 +1,40 @@ +import { React } from "@metro/common"; +import AddonPage from "@core/ui/components/AddonPage"; +import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard"; +import { corePluginInstances, registeredPlugins } from "@lib/addons/plugins"; +import { UnifiedPluginModel } from "@core/ui/settings/pages/Plugins/models"; +import unifyBunnyPlugin from "@core/ui/settings/pages/Plugins/models/bunny"; +import { Strings } from "@core/i18n"; + +/** + * Core plugins settings page + * + * This page shows only the built-in core plugins. Core plugins are always + * installed (can't be uninstalled) and are registered in `corePluginInstances`. + * + * We reuse the existing `AddonPage` + `PluginCard` components and feed them + * the normalized `UnifiedPluginModel` objects produced by `unifyBunnyPlugin`. + */ + +export default function CorePlugins() { + return ( + + CardComponent={PluginCard} + title={Strings.PLUGINS} + searchKeywords={[ + "name", + "description", + (p) => + p.authors + ?.map((a: any) => (typeof a === "string" ? a : a.name)) + .join() || "", + ]} + // Only core plugins: map corePluginInstances -> registeredPlugins -> unify + items={[...corePluginInstances.keys()] + .map((id) => registeredPlugins.get(id)) + .filter((m): m is any => m !== undefined) + .map(unifyBunnyPlugin)} + ListHeaderComponent={() => null} + /> + ); +} diff --git a/src/core/ui/settings/pages/Plugins/index.tsx b/src/core/ui/settings/pages/Plugins/index.tsx index 5a01e56..7d655c7 100644 --- a/src/core/ui/settings/pages/Plugins/index.tsx +++ b/src/core/ui/settings/pages/Plugins/index.tsx @@ -510,7 +510,8 @@ export default function Plugins() { .filter((p) => isPluginInstalled(p.id) && !isCorePlugin(p.id)) .map(unifyBunnyPlugin); - return [...corePlugins, ...vdPlugins, ...bnPlugins]; + // Show only Vendetta plugins + non-core Bunny plugins (exclude core plugins) + return [...vdPlugins, ...bnPlugins]; }} ListHeaderComponent={() => null} installAction={{ diff --git a/src/lib/addons/themes/colors/updater.ts b/src/lib/addons/themes/colors/updater.ts index 600974b..ad57bc8 100644 --- a/src/lib/addons/themes/colors/updater.ts +++ b/src/lib/addons/themes/colors/updater.ts @@ -29,7 +29,7 @@ export const _colorRef: InternalColorRef = { export function updateBunnyColor( colorManifest: ColorManifest | null, - { update = true }, + { update = true, startup = true }, ) { if (settings.safeMode?.enabled) return; @@ -60,6 +60,15 @@ export function updateBunnyColor( } if (update) { + AppearanceManager.setShouldSyncAppearanceSettings(false); + AppearanceManager.updateTheme( + internalDef != null ? ref.key : ref.lastSetDiscordTheme, + ); + } + + if (startup) { + // scuffed but it works + AppearanceManager.setShouldSyncAppearanceSettings(true); AppearanceManager.updateTheme( internalDef != null ? ref.key : ref.lastSetDiscordTheme, ); diff --git a/src/lib/addons/themes/index.ts b/src/lib/addons/themes/index.ts index 94932f9..65d6522 100644 --- a/src/lib/addons/themes/index.ts +++ b/src/lib/addons/themes/index.ts @@ -121,10 +121,10 @@ export function selectTheme(theme: VdThemeInfo | null, write = true) { ); if (theme == null && write) { - updateBunnyColor(null, { update: true }); + updateBunnyColor(null, { update: false, startup: true }); return writeThemeToNative({}); } else if (theme) { - updateBunnyColor(theme.data, { update: true }); + updateBunnyColor(theme.data, { update: true, startup: false }); return writeThemeToNative(theme); } } diff --git a/src/modules.d.ts b/src/modules.d.ts index a39975e..ab9bbbc 100644 --- a/src/modules.d.ts +++ b/src/modules.d.ts @@ -1,5 +1,5 @@ declare module "bunny-build-info" { - const version: "1.1.0-3"; + const version: "1.1.0"; } declare module "*.png" {