feat: port Antied & Antied Zero, add repository-wide blacklist enforcement

This commit is contained in:
Rosie Val 2026-09-06 12:50:51 -04:00
parent 82eec74856
commit d4f1332f5a
45 changed files with 1761 additions and 95 deletions

4
blacklist.json Normal file
View file

@ -0,0 +1,4 @@
{
"updated_at": "2026-09-06T15:25:00.000Z",
"list": []
}

View file

@ -1,4 +1,4 @@
import { readFile, writeFile, readdir } from "fs/promises";
import { readFile, writeFile, readdir, copyFile } from "fs/promises";
import { extname, resolve as resolvePath } from "path";
import { createHash } from "crypto";
@ -108,6 +108,8 @@ for (const plug of await readdir("./plugins")) {
manifest.hash = createHash("sha256").update(toHash).digest("hex");
manifest.main = "index.js";
await writeFile(`./dist/${plug}/install/manifest.json`, JSON.stringify(manifest));
await writeFile(`./dist/${plug}/manifest.json`, JSON.stringify(manifest));
await copyFile(outPath, `./dist/${plug}/index.js`);
console.log(`Successfully built ${manifest.name}!`);
} catch (e) {
@ -115,3 +117,7 @@ for (const plug of await readdir("./plugins")) {
process.exit(1);
}
}
try {
await copyFile("./blacklist.json", "./dist/blacklist.json");
} catch (_) {}

View file

@ -2,7 +2,8 @@
"name": "revenge-plugins",
"private": true,
"scripts": {
"build": "node build.mjs && node scripts/generate-site.mjs"
"build": "node build.mjs && node scripts/generate-site.mjs",
"serve": "node serve.mjs"
},
"dependencies": {
"@swc/helpers": "^0.5.17"

View file

@ -0,0 +1,12 @@
{
"name": "Antied Zero",
"description": "Lightweight, zero-config message logger that keeps deleted messages and edit history visible.",
"authors": [
{ "name": "Angelw0lf" },
{ "name": "Rosie" }
],
"main": "src/index.ts",
"vendetta": {
"icon": "ic_edit_24px"
}
}

View file

@ -0,0 +1,45 @@
import { id } from "@vendetta/plugin";
import { showToast } from "@vendetta/ui/toasts";
import { stopPlugin } from "@vendetta/plugins";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import { patchFluxDispatch } from "./patches/fluxDispatch";
import { patchActionSheet } from "./patches/actionSheet";
import { patchSelfEdit } from "./patches/selfEdit";
import Settings from "./ui/Settings";
const isEnabledRef = { current: false };
const deletedMessageMap = new Map<string, any>();
const cleanups: (() => void)[] = [];
export default {
onLoad: async () => {
if (enforceBlacklistOnLoad(id)) return;
isEnabledRef.current = true;
try {
cleanups.push(patchFluxDispatch(deletedMessageMap, isEnabledRef));
cleanups.push(patchActionSheet(isEnabledRef));
cleanups.push(patchSelfEdit(isEnabledRef));
} catch (err) {
console.error("[ANTIED ZERO] Crash on load:", err);
showToast("[ANTIED ZERO] Failed to load. Check debug log.");
stopPlugin(id);
}
},
onUnload: () => {
isEnabledRef.current = false;
for (const fn of cleanups) {
try {
fn();
} catch (_) {}
}
cleanups.length = 0;
deletedMessageMap.clear();
},
settings: Settings,
};

View file

@ -0,0 +1,87 @@
import { before, after } from "@vendetta/patcher";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
import { React, FluxDispatcher } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { findByProps } from "@vendetta/metro";
const ActionSheet = findByProps("openLazy", "hideActionSheet");
const MessageStore = findByProps("getMessage", "getMessages");
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const { ActionSheetRow } = findByProps("ActionSheetRow") || {};
export const regexEscaper = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export function patchActionSheet(isEnabledRef: { current: boolean }): () => void {
if (!ActionSheet?.openLazy || !ActionSheetRow) return () => {};
return before("openLazy", ActionSheet, ([component, args, actionMessage]: any[]) => {
if (!isEnabledRef.current) return;
try {
const message = actionMessage?.message;
if (args !== "MessageLongPressActionSheet" || !message) return;
component.then((instance: any) => {
const unpatch = after("default", instance, (_: any, comp: any) => {
try {
React.useEffect(() => () => { unpatch(); }, []);
const isReplyBtn = (a: any) => a?.props?.label?.toLowerCase?.() === "reply";
const buttons = findInReactTree(comp, (c: any) => c?.find?.(isReplyBtn));
if (!buttons) return comp;
const position = Math.max(buttons.findIndex(isReplyBtn), buttons.length - 1);
let originalMessage = null;
if (message?.channel_id && message?.id) {
originalMessage = MessageStore?.getMessage?.(message.channel_id, message.id) ||
ChannelMessages?.get?.(message.channel_id)?.get?.(message.id);
}
if (!originalMessage) return comp;
const editedTag = "`[ EDITED ]`";
const separator = new RegExp(regexEscaper(editedTag), "gmi");
if (separator.test(message.content)) {
buttons.splice(
position || 1,
0,
React.createElement(ActionSheetRow, {
label: "Remove Edit History",
subLabel: "Antied Zero",
icon: React.createElement(ActionSheetRow.Icon, {
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("ic_message_edit"),
}),
onPress: () => {
const regexPattern = new RegExp(`(?:\\s${regexEscaper(editedTag)}\\n{2})`, "gm");
const lats = message?.content?.split(regexPattern);
const targetMessage = lats?.[lats.length - 1] ?? message.content;
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("Edit history removed", getAssetIDByName("ic_edit_24px"));
},
})
);
}
} catch (e) {
console.error("[ANTIED ZERO] ActionSheet component error:", e);
}
});
});
} catch (e) {
console.error("[ANTIED ZERO] ActionSheet patch error:", e);
}
});
}

View file

@ -0,0 +1,106 @@
import { before } from "@vendetta/patcher";
import { findByProps } from "@vendetta/metro";
import { FluxDispatcher } from "@vendetta/metro/common";
import { recordToGateway } from "@shared/lib/recordToGateway";
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const MessageStore = findByProps("getMessage", "getMessages");
export function patchFluxDispatch(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
return before("dispatch", FluxDispatcher, (args: any[]) => {
if (!isEnabledRef.current) return;
try {
const ev = args[0];
if (!ev || !ev.type) return;
/* =========================================================
MESSAGE_DELETE
==========================================================*/
if (ev.type === "MESSAGE_DELETE") {
if (ev.otherPluginBypass || ev.manualDelete) return;
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(ev.id) || MessageStore?.getMessage?.(ev.channelId, ev.id) || deletedMessageMap.get(ev.id)?.original;
if (!orig?.author?.id || !orig.author.username) return;
// Ephemeral dismiss from bots check
if (orig.author.bot && (orig.flags === 64 || (orig.flags & 64) === 64)) return;
if (orig.author.bot) return;
// Empty check
if (!orig.content && !orig.attachments?.length && !orig.embeds?.length) return;
if (deletedMessageMap.has(ev.id)) {
ev.type = "MESSAGE_UPDATE";
ev.channelId = orig.channel_id || ev.channelId;
ev.message = {
id: ev.id,
channel_id: orig.channel_id || ev.channelId,
flags: 64,
was_deleted: true,
};
return args;
}
const guildId = ChannelStore?.getChannel?.(orig.channel_id || ev.channelId)?.guild_id;
const gatewayOrig = recordToGateway(orig);
ev.message = {
...gatewayOrig,
content: orig.content,
channel_id: orig.channel_id || ev.channelId,
guild_id: guildId,
flags: 64,
was_deleted: true,
message_reference: orig?.message_reference || orig?.messageReference || null,
};
ev.type = "MESSAGE_UPDATE";
ev.channelId = orig.channel_id || ev.channelId;
ev.optimistic = false;
ev.sendMessageOptions = {};
ev.isPushNotification = false;
deletedMessageMap.set(ev.id, { message: args, original: orig });
return args;
}
/* =========================================================
MESSAGE_UPDATE
==========================================================*/
if (ev.type === "MESSAGE_UPDATE") {
if (ev.otherPluginBypass) return;
const msg = ev.message;
if (!msg || msg.author?.bot) return;
// Only handle real content edits with a valid edited_timestamp
if (!msg.edited_timestamp || msg.edited_timestamp === "invalid_timestamp") return;
const chId = msg.channel_id || ev.channelId;
const id = msg.id || ev.id;
if (!chId || !id) return;
const orig = MessageStore?.getMessage?.(chId, id) || ChannelMessages?.get?.(chId)?.get?.(id);
if (!orig?.author?.id || !orig.author.username) return;
if (!orig.content || !msg.content || msg.content === orig.content) return;
const prefix = "`[ EDITED ]`\n\n";
const gatewayOrig = recordToGateway(orig);
ev.message = {
...gatewayOrig,
...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) {
console.error("[ANTIED ZERO] Flux error:", e);
}
});
}

View file

@ -0,0 +1,23 @@
import { before } from "@vendetta/patcher";
import { findByProps } from "@vendetta/metro";
import { regexEscaper } from "./actionSheet";
const Message = findByProps("sendMessage", "startEditMessage");
export function patchSelfEdit(isEnabledRef: { current: boolean }): () => void {
if (!Message?.startEditMessage) return () => {};
return before("startEditMessage", Message, (args: any[]) => {
if (!isEnabledRef.current || !args?.length) return;
const edited = "`[ EDITED ]`";
const dan = regexEscaper(edited);
const regexPattern = new RegExp(`(?:\\s${dan}\\n{2})`, "gm");
const msg = args[2];
if (typeof msg === "string") {
const lats = msg.split(regexPattern);
args[2] = lats[lats.length - 1];
}
});
}

View file

@ -0,0 +1,16 @@
import { React } from "@vendetta/metro/common";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox";
import { TableRowGroup } from "@shared/ui/table";
export default function Settings() {
return (
<SettingsScaffold>
<TableRowGroup title="Antied Zero">
<NoteBox>
Antied Zero is active and running in zero-config mode. Deleted messages are automatically retained with ephemeral flags, and edit history is tracked in-line.
</NoteBox>
</TableRowGroup>
</SettingsScaffold>
);
}

View file

@ -0,0 +1,12 @@
{
"name": "Cute Moodle (Antied)",
"description": "Keeps deleted messages and edit history visible in chat with customizable highlights, indicators, and actionsheet management.",
"authors": [
{ "name": "Angelw0lf" },
{ "name": "Rosie" }
],
"main": "src/index.ts",
"vendetta": {
"icon": "ic_edit_24px"
}
}

148
plugins/antied/src/index.ts Normal file
View file

@ -0,0 +1,148 @@
import { storage, id } from "@vendetta/plugin";
import { findByProps } from "@vendetta/metro";
import { FluxDispatcher } from "@vendetta/metro/common";
import { stopPlugin } from "@vendetta/plugins";
import { showToast } from "@vendetta/ui/toasts";
import { makeDefaults } from "@shared/lib/utility";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import { patchFluxDispatch } from "./patches/fluxDispatch";
import { patchUpdateRows } from "./patches/updateRows";
import { patchActionSheet } from "./patches/actionSheet";
import { patchSelfEdit } from "./patches/selfEdit";
import { patchContextMenu } from "./patches/contextMenu";
import { patchMessageRecord } from "./patches/messageRecord";
import Settings from "./ui/Settings";
import type { AntiedStorage } from "./lib/types";
const ChannelMessages = findByProps("_channelMessages");
const isEnabledRef = { current: false };
const deletedMessageMap = new Map<string, any>();
const cleanups: (() => void)[] = [];
let intervalPurge: any = null;
const KEEP_NEWEST = 10;
const DELETE_EACH_CYCLE = 140;
function initStorage() {
makeDefaults(storage, {
setting: {
colorpick: false,
customize: false,
ingorelist: false,
patches: false,
text: false,
timestamp: false,
},
switches: {
customizeable: false,
enableMD: true,
enableMU: true,
useBackgroundColor: false,
useSemRawColors: false,
ignoreBots: false,
ignoreOwnMessages: false,
minimalistic: true,
alwaysAdd: false,
darkMode: true,
removeDismissButton: false,
addTimestampForEdits: false,
timestampStyle: "R",
useEphemeralForDeleted: true,
overrideIndicator: false,
useIndicatorForDeleted: false,
useCustomPluginName: false,
},
colors: {
textColor: "#E40303",
backgroundColor: "#FF2C2F",
backgroundColorAlpha: "33",
gutterColor: "#FF2C2F",
gutterColorAlpha: "CC",
semRawColorPrefix: "semanticColors.TEXT_BRAND",
},
inputs: {
deletedMessageBuffer: "This message is deleted",
editedMessageBuffer: "`[ EDITED ]`",
historyToast: "[ANTI ED] History Removed",
ignoredUserList: [],
customPluginName: "ANTIED",
customIndicator: "",
},
misc: {
timestampPos: "BEFORE",
editHistoryIcon: "ic_edit_24px",
},
debug: false,
debugUpdateRows: false,
});
}
export default {
onLoad: async () => {
initStorage();
if (enforceBlacklistOnLoad(id)) return;
isEnabledRef.current = true;
try {
cleanups.push(patchMessageRecord(isEnabledRef));
cleanups.push(patchFluxDispatch(deletedMessageMap, isEnabledRef));
cleanups.push(patchUpdateRows(deletedMessageMap, isEnabledRef));
cleanups.push(patchActionSheet(deletedMessageMap, isEnabledRef));
cleanups.push(patchSelfEdit(isEnabledRef));
cleanups.push(patchContextMenu(isEnabledRef));
} catch (err) {
console.error("[ANTIED] Crash on load:", err);
showToast("[ANTIED] Failed to load patches. Check debug log.");
stopPlugin(id);
return;
}
intervalPurge = setInterval(() => {
if (deletedMessageMap.size <= KEEP_NEWEST) return;
const toDelete = Math.min(DELETE_EACH_CYCLE, deletedMessageMap.size - KEEP_NEWEST);
let i = 0;
for (const key of deletedMessageMap.keys()) {
deletedMessageMap.delete(key);
if (++i >= toDelete) break;
}
}, 15 * 60 * 1000);
},
onUnload: () => {
isEnabledRef.current = false;
if (intervalPurge) clearInterval(intervalPurge);
for (const fn of cleanups) {
try {
fn();
} catch (_) {}
}
cleanups.length = 0;
// Revert faked messages
if (ChannelMessages?._channelMessages) {
for (const channelId in ChannelMessages._channelMessages) {
const arr = ChannelMessages._channelMessages[channelId]?._array;
if (!Array.isArray(arr)) continue;
for (const message of arr) {
if (message?.was_deleted) {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
id: message.id,
channelId: message.channel_id || channelId,
otherPluginBypass: true,
});
}
}
}
}
deletedMessageMap.clear();
},
settings: Settings,
};

View file

@ -0,0 +1,51 @@
export interface AntiedStorage {
setting: {
colorpick: boolean;
customize: boolean;
ingorelist: boolean;
patches: boolean;
text: boolean;
timestamp: boolean;
};
switches: {
customizeable: boolean;
enableMD: boolean;
enableMU: boolean;
useBackgroundColor: boolean;
useSemRawColors: boolean;
ignoreBots: boolean;
ignoreOwnMessages: boolean;
minimalistic: boolean;
alwaysAdd: boolean;
darkMode: boolean;
removeDismissButton: boolean;
addTimestampForEdits: boolean;
timestampStyle: "t" | "T" | "d" | "D" | "f" | "F" | "R";
useEphemeralForDeleted: boolean;
overrideIndicator: boolean;
useIndicatorForDeleted: boolean;
useCustomPluginName: boolean;
};
colors: {
textColor: string;
backgroundColor: string;
backgroundColorAlpha: string;
gutterColor: string;
gutterColorAlpha: string;
semRawColorPrefix: string;
};
inputs: {
deletedMessageBuffer: string;
editedMessageBuffer: string;
historyToast: string;
ignoredUserList: { id: string; username: string }[];
customPluginName: string;
customIndicator: string;
};
misc: {
timestampPos: "BEFORE" | "AFTER";
editHistoryIcon: string;
};
debug: boolean;
debugUpdateRows: boolean;
}

View file

@ -0,0 +1,126 @@
import { before, after } from "@vendetta/patcher";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
import { React, FluxDispatcher } from "@vendetta/metro/common";
import { showToast } from "@vendetta/ui/toasts";
import { storage } from "@vendetta/plugin";
import { findByProps } from "@vendetta/metro";
import type { AntiedStorage } from "../lib/types";
const ActionSheet = findByProps("openLazy", "hideActionSheet");
const MessageStore = findByProps("getMessage", "getMessages");
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const { ActionSheetRow } = findByProps("ActionSheetRow") || {};
export const regexEscaper = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export function patchActionSheet(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
if (!ActionSheet?.openLazy || !ActionSheetRow) return () => {};
return before("openLazy", ActionSheet, ([component, args, actionMessage]: any[]) => {
if (!isEnabledRef.current) return;
try {
const message = actionMessage?.message;
if (args !== "MessageLongPressActionSheet" || !message) return;
component.then((instance: any) => {
const unpatch = after("default", instance, (_: any, comp: any) => {
try {
React.useEffect(() => () => { unpatch(); }, []);
const isReplyBtn = (a: any) => a?.props?.label?.toLowerCase?.() === "reply";
const buttons = findInReactTree(comp, (c: any) => c?.find?.(isReplyBtn));
if (!buttons) return comp;
const position = Math.max(buttons.findIndex(isReplyBtn), buttons.length - 1);
let originalMessage = null;
if (message?.channel_id && message?.id) {
originalMessage = MessageStore?.getMessage?.(message.channel_id, message.id) ||
ChannelMessages?.get?.(message.channel_id)?.get?.(message.id);
}
if (!originalMessage) return comp;
const cfg = storage as AntiedStorage;
const escapedBuffer = regexEscaper(cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`");
const separator = new RegExp(escapedBuffer, "gmi");
const hasBuffer = separator.test(message.content);
if (hasBuffer) {
const targetPos = position || 1;
buttons.splice(
targetPos,
0,
React.createElement(ActionSheetRow, {
label: "Remove Edit History",
subLabel: "Cute Moodle (Antied)",
icon: React.createElement(ActionSheetRow.Icon, {
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("ic_message_edit"),
}),
onPress: () => {
const regexPattern = new RegExp(
`(?:(?:\\s${escapedBuffer}(\\s\\(<t:\\d+:[tTdDfFR]>\\))?\\n{2})|(?:(?:\\s\\(<t:\\d+:[tTdDfFR]>\\) ${escapedBuffer}\\n{2})))`,
"gm"
);
const lats = message?.content?.split(regexPattern);
const targetMessage = lats?.[lats.length - 1] ?? message.content;
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();
if (cfg.inputs?.historyToast) {
showToast(cfg.inputs.historyToast, getAssetIDByName(cfg.misc?.editHistoryIcon || "ic_edit_24px"));
}
},
})
);
}
if (!cfg.switches?.useEphemeralForDeleted && deletedMessageMap.has(message.id)) {
const targetPos = position || 1;
buttons.splice(
targetPos,
0,
React.createElement(ActionSheetRow, {
label: "Remove Deleted Message",
subLabel: "Cute Moodle (Antied)",
isDestructive: true,
icon: React.createElement(ActionSheetRow.Icon, {
source: getAssetIDByName("ic_edit_24px") || getAssetIDByName("TrashIcon"),
}),
onPress: () => {
FluxDispatcher.dispatch({
type: "MESSAGE_DELETE",
guildId: ChannelStore?.getChannel?.(originalMessage.channel_id)?.guild_id,
id: message?.id,
channelId: message?.channel_id,
otherPluginBypass: true,
});
ActionSheet.hideActionSheet();
showToast("[ANTIED] Message Removed", getAssetIDByName("ic_edit_24px"));
},
})
);
}
} catch (e) {
console.error("[ANTIED] ActionSheet component error:", e);
}
});
});
} catch (e) {
console.error("[ANTIED] ActionSheet patch error:", e);
}
});
}

View file

@ -0,0 +1,71 @@
import { after } from "@vendetta/patcher";
import { findByName, findByStoreName } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import { findInReactTree } from "@vendetta/utils";
import { showToast } from "@vendetta/ui/toasts";
import type { AntiedStorage } from "../lib/types";
const PROFILE_MENU_NAMES = ["UserProfileOverflowMenu", "BotUserProfileOverflowMenu"];
const UserStore = findByStoreName("UserStore");
function getMainItems(ret: any): any[] | null {
let items = ret?.props?.items;
if (Array.isArray(items) && Array.isArray(items[0])) return items[0];
items = ret?.props?.children?.props?.items;
if (Array.isArray(items) && Array.isArray(items[0])) return items[0];
const node = findInReactTree(ret, (n: any) => Array.isArray(n?.props?.items) && Array.isArray(n.props.items[0]));
return node?.props?.items?.[0] ?? null;
}
export function patchContextMenu(isEnabledRef: { current: boolean }): () => void {
const unpatches: (() => void)[] = [];
for (const name of PROFILE_MENU_NAMES) {
const mod = findByName(name, false);
if (!mod) continue;
unpatches.push(
after("default", mod, (args: any[], ret: any) => {
if (!isEnabledRef.current) return;
try {
const props = args[0] ?? {};
const userId = props.user?.id || props.userId;
if (!userId) return;
const items = getMainItems(ret);
if (!items) return;
if (items.some((item: any) => item?.label?.includes("(Antied)"))) return;
const cfg = storage as AntiedStorage;
cfg.inputs ??= {} as any;
cfg.inputs.ignoredUserList ??= [];
const isIgnored = cfg.inputs.ignoredUserList.some((u) => u.id === userId);
const user = UserStore?.getUser?.(userId);
const username = user?.username || userId;
items.push({
label: isIgnored ? "Unignore User (Antied)" : "Ignore User (Antied)",
isDestructive: !isIgnored,
action: () => {
if (isIgnored) {
cfg.inputs.ignoredUserList = cfg.inputs.ignoredUserList.filter((u) => u.id !== userId);
showToast(`Unignored ${username} in Antied`);
} else {
cfg.inputs.ignoredUserList.push({ id: userId, username });
showToast(`Ignored ${username} in Antied`);
}
},
});
} catch (e) {
console.error("[ANTIED] Context menu error:", e);
}
})
);
}
return () => {
for (const fn of unpatches) fn();
};
}

View file

@ -0,0 +1,175 @@
import { before } from "@vendetta/patcher";
import { findByProps, findByStoreName } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import { FluxDispatcher } from "@vendetta/metro/common";
import { recordToGateway } from "@shared/lib/recordToGateway";
import type { AntiedStorage } from "../lib/types";
const ChannelStore = findByProps("getChannel", "getDMFromUserId");
const ChannelMessages = findByProps("_channelMessages");
const MessageStore = findByProps("getMessage", "getMessages");
const UserStore = findByStoreName("UserStore");
const AuthStore = findByStoreName("AuthenticationStore") || findByProps("getToken");
function getCurrentUserId(): string | undefined {
return UserStore?.getCurrentUser?.()?.id || AuthStore?.getId?.() || AuthStore?.getCurrentUser?.()?.id;
}
const now = () => Date.now();
const tsStyle = () => {
const s = (storage as AntiedStorage).switches?.timestampStyle;
return s && "tTdDfFR".includes(s) ? s : "R";
};
export function patchFluxDispatch(deletedMessageMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
return before("dispatch", FluxDispatcher, (args: any[]) => {
if (!isEnabledRef.current) return;
try {
const ev = args[0];
if (!ev || !ev.type) return;
const cfg = storage as AntiedStorage;
const currentUserId = getCurrentUserId();
/* =========================================================
MESSAGE_DELETE
==========================================================*/
if (ev.type === "MESSAGE_DELETE") {
if (!cfg.switches?.enableMD || ev.otherPluginBypass || ev.manualDelete) return;
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(ev.id) || MessageStore?.getMessage?.(ev.channelId, ev.id) || deletedMessageMap.get(ev.id)?.original;
if (!orig?.author?.id || !orig.author.username) return;
// Ephemeral message dismiss check
if (orig?.author?.bot && (orig?.flags === 64 || (orig?.flags & 64) === 64)) return;
// Empty message check
if (!orig.content && !orig.attachments?.length && !orig.embeds?.length && !orig.components?.length) return;
// Bot check
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) return;
// Own message check
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) return;
// Ignored users list check
if (cfg.inputs?.ignoredUserList?.length) {
if (cfg.inputs.ignoredUserList.some((u) => u.id === orig.author.id || u.username === orig.author.username)) return;
}
if (deletedMessageMap.has(ev.id)) {
ev.type = "MESSAGE_UPDATE";
ev.channelId = orig.channel_id || ev.channelId;
ev.message = {
id: ev.id,
channel_id: orig.channel_id || ev.channelId,
was_deleted: true,
};
return args;
}
const guildId = ChannelStore?.getChannel?.(orig.channel_id || ev.channelId)?.guild_id;
const gatewayOrig = recordToGateway(orig);
ev.message = {
...gatewayOrig,
content: orig.content,
channel_id: orig.channel_id || ev.channelId,
guild_id: guildId,
was_deleted: true,
message_reference: orig?.message_reference || orig?.messageReference || null,
};
ev.type = "MESSAGE_UPDATE";
ev.channelId = orig.channel_id || ev.channelId;
ev.optimistic = false;
ev.sendMessageOptions = {};
ev.isPushNotification = false;
deletedMessageMap.set(ev.id, { message: args, original: orig });
return args;
}
/* =========================================================
MESSAGE_DELETE_BULK
==========================================================*/
if (ev.type === "MESSAGE_DELETE_BULK") {
if (!cfg.switches?.enableMD || ev.otherPluginBypass || !Array.isArray(ev.ids)) return;
for (const id of ev.ids) {
const orig = ChannelMessages?.get?.(ev.channelId)?.get?.(id) || MessageStore?.getMessage?.(ev.channelId, id);
if (!orig?.author?.id) continue;
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) continue;
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) continue;
deletedMessageMap.set(id, { original: orig });
const gatewayOrig = recordToGateway(orig);
FluxDispatcher.dispatch({
type: "MESSAGE_UPDATE",
channelId: ev.channelId,
message: {
...gatewayOrig,
was_deleted: true,
},
otherPluginBypass: true,
});
}
return;
}
/* =========================================================
MESSAGE_UPDATE
==========================================================*/
if (ev.type === "MESSAGE_UPDATE") {
if (!cfg.switches?.enableMU || ev.otherPluginBypass) return;
const msg = ev.message;
if (!msg) return;
// A MESSAGE_UPDATE is ONLY a real content edit if edited_timestamp is present and not synthetic
if (!msg.edited_timestamp || msg.edited_timestamp === "invalid_timestamp") return;
const chId = msg.channel_id || ev.channelId;
const id = msg.id || ev.id;
if (!chId || !id) return;
const orig = MessageStore?.getMessage?.(chId, id) || ChannelMessages?.get?.(chId)?.get?.(id);
if (!orig?.author?.id) return;
if (cfg.switches.ignoreBots && (orig.author.bot || orig.author.isNonUserBot?.())) return;
if (cfg.switches.ignoreOwnMessages && orig.author.id === currentUserId) return;
if (!orig.content || !msg.content || msg.content === orig.content) return;
if (cfg.inputs?.ignoredUserList?.length) {
if (cfg.inputs.ignoredUserList.some((u) => u.id === orig.author.id || u.username === orig.author.username)) return;
}
const editedTag = cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`";
if (orig.content.includes(editedTag) && orig.content.endsWith(msg.content)) return;
const time = cfg.switches?.addTimestampForEdits ? `(<t:${Math.floor(now() / 1000)}:${tsStyle()}>)` : null;
const tsPos = cfg.misc?.timestampPos === "BEFORE";
let prefix = editedTag;
prefix = time ? (tsPos ? `${time} ${prefix}\n\n` : `${prefix} ${time}\n\n`) : `${prefix}\n\n`;
const gatewayOrig = recordToGateway(orig);
ev.message = {
...gatewayOrig,
...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: any) {
console.error("[ANTIED] Flux dispatch error:", e?.message ?? e);
}
});
}

View file

@ -0,0 +1,40 @@
import { after } from "@vendetta/patcher";
import { findByProps } from "@vendetta/metro";
const MessageRecordUtils = findByProps("updateMessageRecord", "createMessageRecord");
export function patchMessageRecord(isEnabledRef: { current: boolean }): () => void {
if (!MessageRecordUtils) return () => {};
const cleanups: (() => void)[] = [];
cleanups.push(
after("createMessageRecord", MessageRecordUtils, function ([message], record) {
if (isEnabledRef.current && (message?.was_deleted || record?.was_deleted)) {
if (record) record.was_deleted = true;
}
})
);
cleanups.push(
after("updateMessageRecord", MessageRecordUtils, function ([oldRecord, newRecord], record) {
if (isEnabledRef.current && (oldRecord?.was_deleted || newRecord?.was_deleted)) {
if (record) record.was_deleted = true;
}
})
);
if (typeof MessageRecordUtils.updateServerMessage === "function") {
cleanups.push(
after("updateServerMessage", MessageRecordUtils, function ([oldRecord, newRecord], record) {
if (isEnabledRef.current && (oldRecord?.was_deleted || newRecord?.was_deleted)) {
if (record) record.was_deleted = true;
}
})
);
}
return () => {
for (const fn of cleanups) fn();
};
}

View file

@ -0,0 +1,30 @@
import { before } from "@vendetta/patcher";
import { findByProps } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import { regexEscaper } from "./actionSheet";
import type { AntiedStorage } from "../lib/types";
const Message = findByProps("sendMessage", "startEditMessage");
export function patchSelfEdit(isEnabledRef: { current: boolean }): () => void {
if (!Message?.startEditMessage) return () => {};
return before("startEditMessage", Message, (args: any[]) => {
if (!isEnabledRef.current || !args?.length) return;
const cfg = storage as AntiedStorage;
const edited = cfg.inputs?.editedMessageBuffer || "`[ EDITED ]`";
const dan = regexEscaper(edited);
const regexPattern = new RegExp(
`(?:(?:\\s${dan}(\\s\\(<t:\\d+:[tTdDfFR]>\\))?\\n{2})|(?:(?:\\s\\(<t:\\d+:[tTdDfFR]>\\) ${dan}\\n{2})))`,
"gm"
);
const msg = args[2];
if (typeof msg === "string") {
const lats = msg.split(regexPattern);
args[2] = lats[lats.length - 1];
}
});
}

View file

@ -0,0 +1,114 @@
import { ReactNative } from "@vendetta/metro/common";
import { before, after } from "@vendetta/patcher";
import { storage } from "@vendetta/plugin";
import { findByProps, findByName } from "@vendetta/metro";
import { rawFind } from "@shared/lib/rawFind";
import { waitFor } from "@shared/lib/waitFor";
import type { AntiedStorage } from "../lib/types";
const toHex = (v: string | undefined, fallback: string) => {
const s = String(v || "").trim();
const hex = s.startsWith("#") ? s.slice(1) : s;
return /^[0-9a-fA-F]{6}$/.test(hex) ? `#${hex.toUpperCase()}` : fallback;
};
function handleRow(row: any, deletedMessagesMap: Map<string, any>) {
if (!row || row.type !== 1) return;
const msg = row.message;
if (!msg || (!deletedMessagesMap.has(msg.id) && !msg.was_deleted)) return;
const cfg = storage as AntiedStorage;
const {
colors: { textColor, backgroundColor, backgroundColorAlpha, gutterColor, gutterColorAlpha },
switches: { useBackgroundColor, minimalistic },
inputs: { deletedMessageBuffer },
} = cfg;
msg.edited = deletedMessageBuffer || "This message is deleted";
if (!minimalistic) {
msg.textColor = ReactNative.processColor(toHex(textColor, "#E40303"));
}
if (!minimalistic && useBackgroundColor) {
row.backgroundHighlight = {
backgroundColor: ReactNative.processColor(toHex(backgroundColor, "#FF2C2F") + backgroundColorAlpha),
gutterColor: ReactNative.processColor(toHex(gutterColor, "#FF2C2F") + gutterColorAlpha),
};
}
}
function isNativeUpdateRows(m: any): boolean {
return typeof m?.updateRows === "function" && m.updateRows.toString().includes("[native code]");
}
export function patchUpdateRows(deletedMessagesMap: Map<string, any>, isEnabledRef: { current: boolean }): () => void {
const cleanups: (() => void)[] = [];
const applyHook = (target: any) => {
cleanups.push(
before("updateRows", target, (args: any[]) => {
if (!isEnabledRef.current || !args?.length) return;
if (!deletedMessagesMap.size) return;
const raw = args[1];
if (!raw) return;
if (typeof raw === "string") {
try {
const rows = JSON.parse(raw);
let mutated = false;
for (const row of rows) {
if (row?.type === 1 && row?.message && (deletedMessagesMap.has(row.message.id) || row.message.was_deleted)) {
handleRow(row, deletedMessagesMap);
mutated = true;
}
}
if (mutated) {
args[1] = JSON.stringify(rows);
return args;
}
} catch {
return;
}
} else if (Array.isArray(raw)) {
for (const row of raw) handleRow(row, deletedMessagesMap);
} else if (raw && typeof raw === "object" && Array.isArray(raw.rows)) {
for (const row of raw.rows) handleRow(row, deletedMessagesMap);
}
})
);
};
const { NativeModules } = ReactNative;
const DCDChatManager = NativeModules?.DCDChatManager;
if (DCDChatManager?.updateRows) {
applyHook(DCDChatManager);
}
const nativeChat = rawFind<any>(isNativeUpdateRows) || findByProps("updateRows", "getConstants") || findByProps("updateRows");
if (nativeChat && nativeChat !== DCDChatManager) {
applyHook(nativeChat);
} else if (!DCDChatManager) {
const handle = waitFor(
() => rawFind<any>(isNativeUpdateRows) || findByProps("updateRows"),
(target) => applyHook(target)
);
cleanups.push(() => handle.cancel());
}
const RowManager = findByName("RowManager", false) || findByProps("RowManager")?.RowManager;
if (RowManager?.prototype?.generate) {
cleanups.push(
after("generate", RowManager.prototype, (_args: any[], rowObj: any) => {
if (!isEnabledRef.current || !deletedMessagesMap.size) return;
const row = rowObj?.row || rowObj;
handleRow(row, deletedMessagesMap);
})
);
}
return () => {
for (const fn of cleanups) fn();
};
}

View file

@ -0,0 +1,169 @@
import { React, ReactNative as RN } from "@vendetta/metro/common";
import { findByStoreName } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { showToast } from "@vendetta/ui/toasts";
import SettingsScaffold from "@shared/ui/SettingsScaffold";
import NoteBox from "@shared/ui/NoteBox";
import ColorInput from "@shared/ui/ColorInput";
import { TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@shared/ui/table";
import type { AntiedStorage } from "../lib/types";
const UserStore = findByStoreName("UserStore");
const { View, Text, TouchableOpacity, Image } = RN;
export default function Settings() {
useProxy(storage);
const cfg = storage as AntiedStorage;
cfg.switches ??= {} as any;
cfg.inputs ??= {} as any;
cfg.colors ??= {} as any;
cfg.misc ??= {} as any;
const ignoredUsers = cfg.inputs.ignoredUserList || [];
const handleRemoveUser = (id: string) => {
cfg.inputs.ignoredUserList = ignoredUsers.filter((u) => u.id !== id);
showToast("User removed from ignore list", getAssetIDByName("Check"));
};
return (
<SettingsScaffold>
<NoteBox>
Cute Moodle (Antied) keeps deleted messages and edit history visible in your chat with customizable highlights, indicators, and long-press actionsheet tools.
</NoteBox>
<TableRowGroup title="General Logger">
<TableSwitchRow
label="Log deleted messages"
subLabel="Keep deleted messages visible in chat"
value={!!cfg.switches.enableMD}
onValueChange={(v: boolean) => {
cfg.switches.enableMD = v;
}}
/>
<TableSwitchRow
label="Log edited messages"
subLabel="Track edits and original message contents"
value={!!cfg.switches.enableMU}
onValueChange={(v: boolean) => {
cfg.switches.enableMU = v;
}}
/>
<TableSwitchRow
label="Ignore bots"
subLabel="Do not log messages or edits from bot accounts"
value={!!cfg.switches.ignoreBots}
onValueChange={(v: boolean) => {
cfg.switches.ignoreBots = v;
}}
/>
<TableSwitchRow
label="Ignore your own messages"
subLabel="Do not log your own deletions and edits"
value={!!cfg.switches.ignoreOwnMessages}
onValueChange={(v: boolean) => {
cfg.switches.ignoreOwnMessages = v;
}}
/>
</TableRowGroup>
<TableRowGroup title="Styling & Appearance">
<TableSwitchRow
label="Minimalistic mode"
subLabel="Show simple indicators without heavy colored highlights"
value={!!cfg.switches.minimalistic}
onValueChange={(v: boolean) => {
cfg.switches.minimalistic = v;
}}
/>
<TableSwitchRow
label="Use background highlight"
subLabel="Apply colored tint to deleted message backgrounds"
value={!!cfg.switches.useBackgroundColor}
onValueChange={(v: boolean) => {
cfg.switches.useBackgroundColor = v;
}}
/>
{!cfg.switches.minimalistic && (
<>
<ColorInput
label="Text color"
value={cfg.colors.textColor || "#E40303"}
defaultValue="#E40303"
onChange={(hex: string) => {
cfg.colors.textColor = hex;
}}
/>
{cfg.switches.useBackgroundColor && (
<ColorInput
label="Background highlight color"
value={cfg.colors.backgroundColor || "#FF2C2F"}
defaultValue="#FF2C2F"
onChange={(hex: string) => {
cfg.colors.backgroundColor = hex;
}}
/>
)}
</>
)}
<TableSwitchRow
label="Add timestamp for edits"
subLabel="Include relative timestamp on edited versions"
value={!!cfg.switches.addTimestampForEdits}
onValueChange={(v: boolean) => {
cfg.switches.addTimestampForEdits = v;
}}
/>
</TableRowGroup>
<TableRowGroup title="Custom Buffers">
<TextInput
label="Deleted indicator text"
value={cfg.inputs.deletedMessageBuffer || "This message is deleted"}
onChange={(v: string) => {
cfg.inputs.deletedMessageBuffer = v;
}}
/>
<TextInput
label="Edited prefix tag"
value={cfg.inputs.editedMessageBuffer || "`[ EDITED ]`"}
onChange={(v: string) => {
cfg.inputs.editedMessageBuffer = v;
}}
/>
</TableRowGroup>
<TableRowGroup title={`Ignored Users (${ignoredUsers.length})`}>
{ignoredUsers.length === 0 ? (
<View style={{ padding: 16 }}>
<Text style={{ opacity: 0.6, fontSize: 13, color: "#96989d" }}>No users currently ignored.</Text>
</View>
) : (
ignoredUsers.map((u) => {
const user = UserStore?.getUser?.(u.id);
const name = user?.username ? `@${user.username}` : u.username || u.id;
return (
<TableRow
key={u.id}
label={name}
subLabel={`ID: ${u.id}`}
trailing={
<TouchableOpacity onPress={() => handleRemoveUser(u.id)}>
<Image
source={getAssetIDByName("TrashIcon") || getAssetIDByName("ic_trash_24px")}
style={{ width: 20, height: 20, tintColor: "#ff4d4d" }}
/>
</TouchableOpacity>
}
/>
);
})
)}
</TableRowGroup>
</SettingsScaffold>
);
}

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchRowManager from "./patches/rowManager";
import patchTimestamp from "./patches/timestamp";
import Settings from "./ui/Settings";
@ -9,6 +10,8 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
storage.selected ??= "calendar";
storage.customFormat ??= "dddd, MMMM Do YYYY, h:mm:ss a";
storage.separateMessages ??= false;

View file

@ -115,10 +115,15 @@ function patchProfileRoleItem(): () => void {
return () => cleanups.forEach((fn) => fn());
}
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unpatchAll = applyPatches("CopyRoleColor", logger, {
"role pill long-press": patchRolePill,
"profile role item long-press": patchProfileRoleItem

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchChat from "./patches/chat";
import patchName from "./patches/name";
import patchDetails from "./patches/details";
@ -12,6 +13,8 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
storage.tags ??= {};
unpatchAll = applyPatches("Custom User Tags", logger, {

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import { FluxDispatcher } from "@fpte/lib/flux";
import { UserProfileStore, UserStore } from "@fpte/lib/stores";
import {
@ -31,6 +32,8 @@ let unpatchAll: () => void = () => {};
export default {
onLoad() {
if (enforceBlacklistOnLoad(id)) return;
// The "real" picker chains Discord's own hooks onto this fiber outside React's normal
// reconciliation, risking a "Rendered more/fewer hooks" crash - default to the safer,
// self-contained fallback picker instead.

View file

@ -1,5 +1,10 @@
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import Settings from "./ui/Settings";
export default {
onLoad: () => {
enforceBlacklistOnLoad(id);
},
settings: Settings
};

View file

@ -1,5 +1,6 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import Settings from "./ui/Settings";
import { patchFluxIntercept, rehydrateFromLog, revertFakedMessages } from "./patches/fluxIntercept";
import { patchRowStyling } from "./patches/rowStyling";
@ -29,6 +30,8 @@ function initStorage() {
export default {
onLoad() {
if (enforceBlacklistOnLoad(id)) return;
initStorage();
console.log(TAG, "onLoad");

View file

@ -1,3 +1,5 @@
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import loadCommands from "./commands";
import Settings from "./ui/Settings";
@ -5,6 +7,7 @@ let unregisterFns: (() => void)[] = [];
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unregisterFns = loadCommands();
},
onUnload: () => unregisterFns.forEach((fn) => fn()),

View file

@ -1,5 +1,7 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import Settings from "./ui/Settings";
import patchMoreAltsSection, { SectionRow } from "./patches/sidebarPin";
import patchContextMenu from "./patches/contextMenu";
@ -28,6 +30,7 @@ export default {
settings: Settings,
onLoad() {
if (enforceBlacklistOnLoad(id)) return;
ensureStorage();
try {

View file

@ -1,7 +1,9 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { after } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
import { rawFindByTypeName } from "@shared/lib/rawFind";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import PronounSection from "./ui/PronounSection";
import Settings from "./ui/Settings";
@ -68,6 +70,7 @@ function attempt() {
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
attempt();
if (!patched) {
let ticks = 0;

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchRing from "./patches/ringPatch";
import Settings from "./ui/Settings";
@ -8,6 +9,8 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
// Force re-enabling once, superseding the old force-disable flag from before the growth
// model in ringPatch.ts was fixed to additive px instead of a percentage multiplier.
if (storage.forceReenabledV3 !== true) {

View file

@ -1,3 +1,5 @@
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import loadCommands from "./commands";
import { startScheduler, stopScheduler } from "./lib/reminders";
import Settings from "./ui/Settings";
@ -6,6 +8,7 @@ let unregisterFns: (() => void)[] = [];
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unregisterFns = loadCommands();
startScheduler();
},

View file

@ -1,9 +1,12 @@
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import loadCommands from "./commands";
let unregisterFns: (() => void)[] = [];
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unregisterFns = loadCommands();
},
onUnload: () => unregisterFns.forEach((fn) => fn())

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchRows from "./patches/rows";
import patchMemberList from "./patches/memberList";
import patchTypingWrapper from "./patches/typingWrapper";
@ -11,6 +12,7 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
storage.noMention ??= false;
storage.noRole ??= false;
storage.noVoice ??= false;

View file

@ -1,5 +1,7 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import Settings from "./ui/Settings";
import PluginsBrowser from "./ui/PluginsBrowser";
import patchRosiesPlugsSection, { SectionRow } from "./patches/settings";
@ -38,6 +40,7 @@ function buildRows(): SectionRow[] {
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
try {
unpatch = patchRosiesPlugsSection(buildRows);
} catch (e: any) {

View file

@ -1,10 +1,13 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import modules from "./modules";
import { vstorage } from "./lib/Module";
import Settings from "./ui/Settings";
export default {
onLoad() {
if (enforceBlacklistOnLoad(id)) return;
vstorage.modules ??= {};
for (const module of modules) {

View file

@ -1,8 +1,9 @@
import { findByProps, findByStoreName } from "@vendetta/metro";
import { instead } from "@vendetta/patcher";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { showToast } from "@vendetta/ui/toasts";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import { intoChunks } from "./lib/split";
import Settings from "./ui/Settings";
@ -18,6 +19,7 @@ let unpatchEdit: (() => boolean) | undefined;
export default {
onLoad() {
if (enforceBlacklistOnLoad(id)) return;
storage.splitOnWords ??= false;
const MaxLengthModule = findByProps("getMaxMessageLength");

View file

@ -1,6 +1,7 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchChat from "./patches/chat";
import patchDetails from "./patches/details";
import patchName from "./patches/name";
@ -12,6 +13,7 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
storage.useRoleColor ??= false;
storage.tags ??= {};

View file

@ -1,11 +1,14 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchTypingIndicator from "./patches/typingIndicator";
let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unpatchAll = applyPatches("TypingAvatars", logger, {
"typing indicator avatars": patchTypingIndicator
});

View file

@ -1,9 +1,12 @@
import { id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import loadCommands from "./commands";
let unregisterFns: (() => void)[] = [];
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
unregisterFns = loadCommands();
},
onUnload: () => unregisterFns.forEach((fn) => fn())

View file

@ -1,11 +1,13 @@
// Forked from bwlok/revenge-plugins (plugins/ViewRaw), GPLv3. Original authors: sapphire, Vendicated, Bwlok.
// https://github.com/bwlok/revenge-plugins/tree/master/plugins/ViewRaw
import { before, after } from "@vendetta/patcher";
import { id } from "@vendetta/plugin";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
import { findByName, findByProps } from "@vendetta/metro";
import { React } from "@vendetta/metro/common";
import { Forms } from "@vendetta/ui/components";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import RawPage from "./RawPage";
import { recordDetection } from "./lib/diagnostics";
import Settings from "./ui/Settings";
@ -66,97 +68,101 @@ function actionSheetRowButton(navigator: () => JSX.Element) {
const patches: (() => void)[] = [];
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
component.then((instance: any) => {
// Discord reuses the same lazy-loaded instance across opens - patch once, track the active
// message on the instance itself.
instance.__viewRawActiveMessage = msg.message;
if (instance.__viewRawPatched) return;
instance.__viewRawPatched = true;
const unpatchOpenLazy = before("openLazy", LazyActionSheet, ([component, key, msg]) => {
if (key !== "MessageLongPressActionSheet" || !msg?.message) return;
const unpatchDefault = after("default", instance, (_: any, component: any) => {
try {
if (!Navigator) {
console.log("[ViewRaw] Error: Navigator not found - can't open RawPage");
recordDetection("none", "Navigator not found");
return;
}
component.then((instance: any) => {
instance.__viewRawActiveMessage = msg.message;
if (instance.__viewRawPatched) return;
instance.__viewRawPatched = true;
const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
// Strategy 1: a plain list of buttons (older/simpler layouts).
const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
if (buttons) {
buttons.push(formRowButton(navigator));
recordDetection("buttons");
return;
}
// Strategy 2: real ActionSheetRow groups, falling back to a new group if none match.
const groups = findInReactTree(
component,
(x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
);
if (Array.isArray(groups) && groups.length && ActionSheetRow) {
const button = actionSheetRowButton(navigator);
let inserted = false;
for (const group of groups) {
const children = findInReactTree(
group,
(c) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
);
if (Array.isArray(children)) {
children.push(button);
inserted = true;
break;
const unpatchDefault = after("default", instance, (_: any, component: any) => {
try {
if (!Navigator) {
console.log("[ViewRaw] Error: Navigator not found - can't open RawPage");
recordDetection("none", "Navigator not found");
return;
}
const navigator = buildNavigator(() => instance.__viewRawActiveMessage);
// Strategy 1: a plain list of buttons (older/simpler layouts).
const buttons = findInReactTree(component, (x) => x?.[0]?.type?.name === "ButtonRow");
if (buttons) {
buttons.push(formRowButton(navigator));
recordDetection("buttons");
return;
}
// Strategy 2: real ActionSheetRow groups, falling back to a new group if none match.
const groups = findInReactTree(
component,
(x) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup",
);
if (Array.isArray(groups) && groups.length && ActionSheetRow) {
const button = actionSheetRowButton(navigator);
let inserted = false;
for (const group of groups) {
const children = findInReactTree(
group,
(c) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"),
);
if (Array.isArray(children)) {
children.push(button);
inserted = true;
break;
}
}
if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
groups.unshift(<ActionSheetRow.Group>{button}</ActionSheetRow.Group>);
inserted = true;
}
if (inserted) {
recordDetection("actionSheetRow");
return;
}
}
// Strategy 3: name-independent fallback matching on shape (label + onPress) instead.
const genericRowGroup = findInReactTree(
component,
(x) =>
Array.isArray(x) &&
x.length > 0 &&
x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
);
if (Array.isArray(genericRowGroup)) {
genericRowGroup.push(formRowButton(navigator));
recordDetection("generic");
return;
}
console.log("[ViewRaw] Error: Could not find ActionSheet");
recordDetection("none");
} catch (e) {
console.log("[ViewRaw] Error: Failed to add View Raw button", e);
recordDetection("none", String(e));
}
});
if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) {
groups.unshift(<ActionSheetRow.Group>{button}</ActionSheetRow.Group>);
inserted = true;
}
if (inserted) {
recordDetection("actionSheetRow");
return;
}
}
// Strategy 3: name-independent fallback matching on shape (label + onPress) instead.
const genericRowGroup = findInReactTree(
component,
(x) =>
Array.isArray(x) &&
x.length > 0 &&
x.every((el: any) => typeof el?.props?.label === "string" && typeof el?.props?.onPress === "function"),
);
if (Array.isArray(genericRowGroup)) {
genericRowGroup.push(formRowButton(navigator));
recordDetection("generic");
return;
}
console.log("[ViewRaw] Error: Could not find ActionSheet");
recordDetection("none");
} catch (e) {
console.log("[ViewRaw] Error: Failed to add View Raw button", e);
recordDetection("none", String(e));
}
patches.push(unpatchDefault);
});
});
patches.push(unpatchDefault);
});
});
patches.push(unpatchOpenLazy);
export default {
onUnload: () => patches.forEach((unpatch) => unpatch()),
patches.push(unpatchOpenLazy);
},
onUnload: () => {
patches.forEach((unpatch) => unpatch());
patches.length = 0;
},
settings: Settings,
};

View file

@ -1,5 +1,6 @@
import { logger } from "@vendetta";
import { storage } from "@vendetta/plugin";
import { storage, id } from "@vendetta/plugin";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchYouBarButtons from "./patches/youBarButtons";
import { setInboxTracking } from "./lib/notifications";
import Settings from "./ui/Settings";
@ -33,6 +34,7 @@ function attempt() {
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
storage.showDMButton ??= false;
storage.showSettingsButton ??= true;
storage.showInboxButton ??= false;

View file

@ -1,5 +1,7 @@
import { logger } from "@vendetta";
import { id } from "@vendetta/plugin";
import { applyPatches } from "@shared/lib/patcher";
import { enforceBlacklistOnLoad } from "@shared/lib/blacklist";
import patchAutoProcess from "./patches/autoProcess";
import { zStorage } from "./lib/api";
import Settings from "./ui/Settings";
@ -8,6 +10,7 @@ let unpatchAll: () => void = () => {};
export default {
onLoad: () => {
if (enforceBlacklistOnLoad(id)) return;
zStorage();
unpatchAll = applyPatches("Zipline", logger, {

112
serve.mjs Normal file
View file

@ -0,0 +1,112 @@
import http from "http";
import fs from "fs";
import path from "path";
import os from "os";
const PORT = 8000;
const DIST_DIR = path.resolve("./dist");
const MIME_TYPES = {
".html": "text/html",
".js": "application/javascript",
".json": "application/json",
".css": "text/css",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
".txt": "text/plain",
};
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "*");
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
const safeUrl = decodeURIComponent(req.url?.split("?")[0] || "/");
let filePath = path.join(DIST_DIR, safeUrl);
// Root blacklist.json fallback
if (safeUrl === "/blacklist.json") {
const rootBlacklist = path.resolve("./blacklist.json");
if (fs.existsSync(rootBlacklist)) {
filePath = rootBlacklist;
}
}
const PRIVATE_DIST_DIR = path.resolve("../revenge-plugins-private/dist");
// Direct /<plugin>/manifest.json or /<plugin>/index.js routing to install/ directory if needed
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
const parts = safeUrl.split("/").filter(Boolean);
if (parts.length === 2 && (parts[1] === "manifest.json" || parts[1] === "index.js")) {
const installFilePath = path.join(DIST_DIR, parts[0], "install", parts[1]);
if (fs.existsSync(installFilePath) && fs.statSync(installFilePath).isFile()) {
filePath = installFilePath;
} else {
const privateInstallFilePath = path.join(PRIVATE_DIST_DIR, parts[0], "install", parts[1]);
if (fs.existsSync(privateInstallFilePath) && fs.statSync(privateInstallFilePath).isFile()) {
filePath = privateInstallFilePath;
}
}
} else if (fs.existsSync(path.join(PRIVATE_DIST_DIR, safeUrl))) {
filePath = path.join(PRIVATE_DIST_DIR, safeUrl);
}
}
// Directory routing:
// If requesting /<plugin>/install or /<plugin>/install/, serve manifest.json
// Otherwise serve index.html
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
if (filePath.endsWith("install") || filePath.endsWith("install" + path.sep)) {
filePath = path.join(filePath, "manifest.json");
} else {
filePath = path.join(filePath, "index.html");
}
}
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
console.log(`[404] Not Found: ${req.url} -> ${filePath}`);
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 Not Found");
return;
}
const ext = path.extname(filePath).toLowerCase();
const contentType = MIME_TYPES[ext] || "application/octet-stream";
res.writeHead(200, {
"Content-Type": contentType,
"Cache-Control": "no-cache, no-store, must-revalidate",
});
const stream = fs.createReadStream(filePath);
stream.pipe(res);
});
server.listen(PORT, "0.0.0.0", () => {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const iface of Object.values(interfaces)) {
if (!iface) continue;
for (const alias of iface) {
if (alias.family === "IPv4" && !alias.internal) {
addresses.push(alias.address);
}
}
}
console.log(`\n🚀 Server listening on port ${PORT} (0.0.0.0)`);
console.log(`Accessible URLs:`);
for (const addr of addresses) {
console.log(` - http://${addr}:${PORT}/`);
console.log(` • Antied: http://${addr}:${PORT}/antied/install/`);
console.log(` • Antied Zero: http://${addr}:${PORT}/antied-zero/install/`);
}
console.log(`\nReady for device testing via Tailscale / LAN!\n`);
});

91
shared/lib/blacklist.ts Normal file
View file

@ -0,0 +1,91 @@
import { logger, plugin } from "@vendetta";
import { findByStoreName, findByProps } from "@vendetta/metro";
import { removePlugin } from "@vendetta/plugins";
import { safeFetch } from "@vendetta/utils";
import { showToast } from "@vendetta/ui/toasts";
const UserStore = findByStoreName("UserStore");
const AuthStore = findByStoreName("AuthenticationStore") || findByProps("getToken");
export const DEFAULT_BLACKLIST_URL = "https://raw.githubusercontent.com/everestmcarthur/revenge-plugins/main/blacklist.json";
/**
* Local fallback blacklist: Any Discord User IDs hardcoded here.
*/
export const LOCAL_BLACKLIST: string[] = [];
function getCurrentUserId(): string | undefined {
return UserStore?.getCurrentUser?.()?.id || AuthStore?.getId?.() || AuthStore?.getCurrentUser?.()?.id;
}
/** Grab list from remote url, cache it, and remove self if the user is blacklisted */
export async function fetchBlacklist(url: string = DEFAULT_BLACKLIST_URL): Promise<{ list: (string | number)[] }> {
try {
const bustUrl = url.includes("?") ? `${url}&_t=${Date.now()}` : `${url}?_t=${Date.now()}`;
const res = await safeFetch(bustUrl, { cache: "no-store" });
if (res.ok) {
const data = await res.json();
return { list: Array.isArray(data?.list) ? data.list : [] };
}
} catch (e: any) {
logger.info("[Blacklist] Unable to fetch remote blacklist data:", e?.message ?? e);
}
return { list: [] };
}
/** Check if user is in local or remote blacklist and trigger self-deletion if matched */
export function checkAndEnforceBlacklist(
blocklist?: { list?: (string | number)[] },
pluginId: string = plugin?.id,
delaySeconds = 15
): boolean {
const myId = getCurrentUserId();
if (!myId) return false;
// Combine local static blacklist with remote blacklist
const combined = new Set<string>([
...LOCAL_BLACKLIST.map(String),
...(blocklist?.list?.map(String) || []),
]);
if (!combined.size) return false;
const isBlacklisted = combined.has(String(myId));
if (isBlacklisted && pluginId) {
logger.warn(`[Blacklist] User ${myId} is blacklisted. Plugin will self-delete in ${delaySeconds}s.`);
try {
showToast(`[Blacklist] You are blacklisted from using this plugin. Self-deleting in ${delaySeconds}s.`);
} catch (_) {}
setTimeout(() => {
logger.info("[Blacklist] Removing blacklisted plugin:", pluginId);
try {
removePlugin(pluginId);
} catch (e: any) {
logger.error("[Blacklist] Failed to remove plugin:", e?.message ?? e);
}
}, delaySeconds * 1000);
return true;
}
return false;
}
/**
* Standard 1-line enforcement for any plugin's onLoad lifecycle.
* Checks local blacklist immediately, then fetches remote in background.
*/
export function enforceBlacklistOnLoad(pluginId: string = plugin?.id, delaySeconds = 15): boolean {
if (checkAndEnforceBlacklist(undefined, pluginId, delaySeconds)) {
return true;
}
fetchBlacklist(DEFAULT_BLACKLIST_URL)
.then((blocklist) => {
checkAndEnforceBlacklist(blocklist, pluginId, delaySeconds);
})
.catch((e) => {
logger.info("[Blacklist] Remote check failed:", e?.message ?? e);
});
return false;
}

View file

@ -0,0 +1,85 @@
export function authorToGateway(a: any): any {
if (!a || typeof a !== "object") return a;
return {
id: a.id,
username: a.username,
discriminator: a.discriminator && a.discriminator !== "???" ? String(a.discriminator) : "0",
avatar: a.avatar ?? null,
avatar_decoration_data: a.avatarDecorationData ?? a.avatar_decoration_data ?? null,
bot: Boolean(a.bot),
global_name: a.globalName ?? a.global_name ?? a.username,
};
}
export function embedToGateway(e: any): any {
if (!e || typeof e !== "object") return e;
return {
...e,
title: e.rawTitle ?? e.title,
description: e.rawDescription ?? e.description,
fields: Array.isArray(e.fields)
? e.fields.map((f: any) => ({
name: f.rawName ?? f.name ?? "",
value: f.rawValue ?? f.value ?? "",
inline: Boolean(f.inline),
}))
: undefined,
author: e.author
? {
name: e.author.name,
url: e.author.url,
icon_url: e.author.iconURL ?? e.author.icon_url,
proxy_icon_url: e.author.iconProxyURL ?? e.author.proxy_icon_url,
}
: undefined,
footer: e.footer
? {
text: e.footer.text,
icon_url: e.footer.iconURL ?? e.footer.icon_url,
proxy_icon_url: e.footer.iconProxyURL ?? e.footer.proxy_icon_url,
}
: undefined,
image: e.image
? {
url: e.image.url,
proxy_url: e.image.proxyURL ?? e.image.proxy_url,
width: e.image.width,
height: e.image.height,
}
: undefined,
thumbnail: e.thumbnail
? {
url: e.thumbnail.url,
proxy_url: e.thumbnail.proxyURL ?? e.thumbnail.proxy_url,
width: e.thumbnail.width,
height: e.thumbnail.height,
}
: undefined,
};
}
export function componentToGateway(c: any): any {
if (!c || typeof c !== "object") return c;
const copy = { ...c };
if (copy.type === 3 || copy.type === "3") {
copy.custom_id = copy.customId ?? copy.custom_id ?? copy.id ?? "select";
copy.min_values = copy.minValues ?? copy.min_values ?? 1;
copy.max_values = copy.maxValues ?? copy.max_values ?? 1;
} else if (copy.type === 2 || copy.type === "2") {
copy.custom_id = copy.customId ?? copy.custom_id ?? copy.id ?? "button";
}
if (Array.isArray(copy.components)) {
copy.components = copy.components.map(componentToGateway);
}
return copy;
}
export function recordToGateway(record: any): any {
if (!record || typeof record !== "object") return record;
return {
...record,
author: authorToGateway(record.author),
embeds: Array.isArray(record.embeds) ? record.embeds.map(embedToGateway) : record.embeds,
components: Array.isArray(record.components) ? record.components.map(componentToGateway) : record.components,
};
}

73
shared/lib/utility.ts Normal file
View file

@ -0,0 +1,73 @@
import { findByProps } from "@vendetta/metro";
import { logger } from "@vendetta";
import { showToast } from "@vendetta/ui/toasts";
const { openLazy } = findByProps("openLazy", "hideActionSheet") || {};
export function makeDefaults(object: any, defaults: any) {
if (object != null && defaults != null) {
for (const key of Object.keys(defaults)) {
if (typeof defaults[key] === "object" && !Array.isArray(defaults[key]) && defaults[key] !== null) {
if (typeof object[key] !== "object" || object[key] === null) object[key] = {};
makeDefaults(object[key], defaults[key]);
} else {
object[key] ??= defaults[key];
}
}
}
}
export function openSheet(sheet: any, props?: any) {
try {
if (openLazy) {
openLazy(
Promise.resolve({ default: sheet }),
"ActionSheet",
props
);
}
} catch (e: any) {
logger.error(e?.stack ?? e);
showToast("Error opening ActionSheet");
}
}
export const setOpacity = (hex: string, alpha: number) =>
`${hex}${Math.floor(alpha * 255).toString(16).padStart(2, "0")}`;
export const colorConverter = {
toInt(hex: string) {
hex = hex.replace(/^#/, "");
return parseInt(hex, 16);
},
toHex(integer: number) {
const hex = integer.toString(16).toUpperCase();
return "#" + hex;
},
HSLtoHEX(h: number, s: number, l: number) {
l /= 100;
const a = (s * Math.min(l, 1 - l)) / 100;
const f = (n: number) => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, "0");
};
return `#${f(0)}${f(8)}${f(4)}`;
},
};
export function validateHex(input: string | undefined | null, defaultColor = "#000000") {
if (!input) return defaultColor;
const trimmedInput = input.trim();
if (trimmedInput.startsWith("#")) {
const hexCode = trimmedInput.slice(1);
if (/^[0-9A-Fa-f]{6}$/.test(hexCode)) {
return "#" + hexCode.toUpperCase();
}
} else {
if (/^[0-9A-Fa-f]{6}$/.test(trimmedInput)) {
return "#" + trimmedInput.toUpperCase();
}
}
return defaultColor;
}