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
This commit is contained in:
parent
3f01f858e9
commit
6b7ec92149
16 changed files with 889 additions and 116 deletions
|
|
@ -74,7 +74,7 @@ const config = {
|
|||
constModules: {
|
||||
globals: {
|
||||
"bunny-build-info": {
|
||||
version: `"1.1.0-3"`,
|
||||
version: `"1.1.0"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 () => <ApplicationCommand>{
|
||||
/**
|
||||
* /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 () =>
|
||||
<ApplicationCommand>{
|
||||
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<string>;
|
||||
enabled: boolean; // overall enabled if any source reports enabled (Vendetta) or pluginSettings for Bunny
|
||||
};
|
||||
|
||||
const aggregated = new Map<string, ExtEntry>();
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, any>;
|
||||
|
||||
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<BunnyBadge[]>([]);
|
||||
|
||||
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<string, any>;
|
||||
|
||||
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<BunnyBadge[]>([]);
|
||||
|
||||
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: " _",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ export const getCorePlugins = (): Record<string, CorePlugin> => ({
|
|||
"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") },
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)" }],
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
43
src/core/plugins/messagelogger/index.jsx
Normal file
43
src/core/plugins/messagelogger/index.jsx
Normal file
|
|
@ -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 = [];
|
||||
},
|
||||
});
|
||||
139
src/core/plugins/messagelogger/patches/actionsheet.jsx
Normal file
139
src/core/plugins/messagelogger/patches/actionsheet.jsx
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
125
src/core/plugins/messagelogger/patches/flux_dispatch.js
Normal file
125
src/core/plugins/messagelogger/patches/flux_dispatch.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
21
src/core/plugins/messagelogger/patches/self_edit.js
Normal file
21
src/core/plugins/messagelogger/patches/self_edit.js
Normal file
|
|
@ -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;
|
||||
});
|
||||
226
src/core/plugins/shiggyhelper/index.tsx
Normal file
226
src/core/plugins/shiggyhelper/index.tsx
Normal file
|
|
@ -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<boolean>(
|
||||
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");
|
||||
},
|
||||
});
|
||||
|
|
@ -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"),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
40
src/core/ui/settings/pages/Plugins/CorePlugins.tsx
Normal file
40
src/core/ui/settings/pages/Plugins/CorePlugins.tsx
Normal file
|
|
@ -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 (
|
||||
<AddonPage<UnifiedPluginModel>
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
src/modules.d.ts
vendored
2
src/modules.d.ts
vendored
|
|
@ -1,5 +1,5 @@
|
|||
declare module "bunny-build-info" {
|
||||
const version: "1.1.0-3";
|
||||
const version: "1.1.0";
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue