diff --git a/blacklist.json b/blacklist.json new file mode 100644 index 0000000..1da16db --- /dev/null +++ b/blacklist.json @@ -0,0 +1,4 @@ +{ + "updated_at": "2026-09-06T15:25:00.000Z", + "list": [] +} diff --git a/build.mjs b/build.mjs index 601d24d..71c6905 100644 --- a/build.mjs +++ b/build.mjs @@ -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 (_) {} diff --git a/package.json b/package.json index 57d44b2..a3cc560 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/plugins/antied-zero/manifest.json b/plugins/antied-zero/manifest.json new file mode 100644 index 0000000..289826c --- /dev/null +++ b/plugins/antied-zero/manifest.json @@ -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" + } +} diff --git a/plugins/antied-zero/src/index.ts b/plugins/antied-zero/src/index.ts new file mode 100644 index 0000000..3bbfa47 --- /dev/null +++ b/plugins/antied-zero/src/index.ts @@ -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(); +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, +}; diff --git a/plugins/antied-zero/src/patches/actionSheet.tsx b/plugins/antied-zero/src/patches/actionSheet.tsx new file mode 100644 index 0000000..c9ef4b1 --- /dev/null +++ b/plugins/antied-zero/src/patches/actionSheet.tsx @@ -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); + } + }); +} diff --git a/plugins/antied-zero/src/patches/fluxDispatch.ts b/plugins/antied-zero/src/patches/fluxDispatch.ts new file mode 100644 index 0000000..a7ce293 --- /dev/null +++ b/plugins/antied-zero/src/patches/fluxDispatch.ts @@ -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, 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); + } + }); +} diff --git a/plugins/antied-zero/src/patches/selfEdit.ts b/plugins/antied-zero/src/patches/selfEdit.ts new file mode 100644 index 0000000..bfb56c3 --- /dev/null +++ b/plugins/antied-zero/src/patches/selfEdit.ts @@ -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]; + } + }); +} diff --git a/plugins/antied-zero/src/ui/Settings.tsx b/plugins/antied-zero/src/ui/Settings.tsx new file mode 100644 index 0000000..691eeb5 --- /dev/null +++ b/plugins/antied-zero/src/ui/Settings.tsx @@ -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 ( + + + + 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. + + + + ); +} diff --git a/plugins/antied/manifest.json b/plugins/antied/manifest.json new file mode 100644 index 0000000..a7c031d --- /dev/null +++ b/plugins/antied/manifest.json @@ -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" + } +} diff --git a/plugins/antied/src/index.ts b/plugins/antied/src/index.ts new file mode 100644 index 0000000..7996108 --- /dev/null +++ b/plugins/antied/src/index.ts @@ -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(); +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, +}; diff --git a/plugins/antied/src/lib/types.ts b/plugins/antied/src/lib/types.ts new file mode 100644 index 0000000..b59174d --- /dev/null +++ b/plugins/antied/src/lib/types.ts @@ -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; +} diff --git a/plugins/antied/src/patches/actionSheet.tsx b/plugins/antied/src/patches/actionSheet.tsx new file mode 100644 index 0000000..fa70ddc --- /dev/null +++ b/plugins/antied/src/patches/actionSheet.tsx @@ -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, 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\\(\\))?\\n{2})|(?:(?:\\s\\(\\) ${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); + } + }); +} diff --git a/plugins/antied/src/patches/contextMenu.ts b/plugins/antied/src/patches/contextMenu.ts new file mode 100644 index 0000000..423176a --- /dev/null +++ b/plugins/antied/src/patches/contextMenu.ts @@ -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(); + }; +} diff --git a/plugins/antied/src/patches/fluxDispatch.ts b/plugins/antied/src/patches/fluxDispatch.ts new file mode 100644 index 0000000..b59c827 --- /dev/null +++ b/plugins/antied/src/patches/fluxDispatch.ts @@ -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, 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 ? `()` : 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); + } + }); +} diff --git a/plugins/antied/src/patches/messageRecord.ts b/plugins/antied/src/patches/messageRecord.ts new file mode 100644 index 0000000..4f7be01 --- /dev/null +++ b/plugins/antied/src/patches/messageRecord.ts @@ -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(); + }; +} diff --git a/plugins/antied/src/patches/selfEdit.ts b/plugins/antied/src/patches/selfEdit.ts new file mode 100644 index 0000000..fba5da2 --- /dev/null +++ b/plugins/antied/src/patches/selfEdit.ts @@ -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\\(\\))?\\n{2})|(?:(?:\\s\\(\\) ${dan}\\n{2})))`, + "gm" + ); + + const msg = args[2]; + if (typeof msg === "string") { + const lats = msg.split(regexPattern); + args[2] = lats[lats.length - 1]; + } + }); +} diff --git a/plugins/antied/src/patches/updateRows.ts b/plugins/antied/src/patches/updateRows.ts new file mode 100644 index 0000000..735194f --- /dev/null +++ b/plugins/antied/src/patches/updateRows.ts @@ -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) { + 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, 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(isNativeUpdateRows) || findByProps("updateRows", "getConstants") || findByProps("updateRows"); + if (nativeChat && nativeChat !== DCDChatManager) { + applyHook(nativeChat); + } else if (!DCDChatManager) { + const handle = waitFor( + () => rawFind(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(); + }; +} diff --git a/plugins/antied/src/ui/Settings.tsx b/plugins/antied/src/ui/Settings.tsx new file mode 100644 index 0000000..7279251 --- /dev/null +++ b/plugins/antied/src/ui/Settings.tsx @@ -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 ( + + + Cute Moodle (Antied) keeps deleted messages and edit history visible in your chat with customizable highlights, indicators, and long-press actionsheet tools. + + + + { + cfg.switches.enableMD = v; + }} + /> + { + cfg.switches.enableMU = v; + }} + /> + { + cfg.switches.ignoreBots = v; + }} + /> + { + cfg.switches.ignoreOwnMessages = v; + }} + /> + + + + { + cfg.switches.minimalistic = v; + }} + /> + { + cfg.switches.useBackgroundColor = v; + }} + /> + {!cfg.switches.minimalistic && ( + <> + { + cfg.colors.textColor = hex; + }} + /> + {cfg.switches.useBackgroundColor && ( + { + cfg.colors.backgroundColor = hex; + }} + /> + )} + + )} + { + cfg.switches.addTimestampForEdits = v; + }} + /> + + + + { + cfg.inputs.deletedMessageBuffer = v; + }} + /> + { + cfg.inputs.editedMessageBuffer = v; + }} + /> + + + + {ignoredUsers.length === 0 ? ( + + No users currently ignored. + + ) : ( + ignoredUsers.map((u) => { + const user = UserStore?.getUser?.(u.id); + const name = user?.username ? `@${user.username}` : u.username || u.id; + return ( + handleRemoveUser(u.id)}> + + + } + /> + ); + }) + )} + + + ); +} diff --git a/plugins/better-timestamps/src/index.ts b/plugins/better-timestamps/src/index.ts index 53f601b..6ea9294 100644 --- a/plugins/better-timestamps/src/index.ts +++ b/plugins/better-timestamps/src/index.ts @@ -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; diff --git a/plugins/copy-role-color/src/index.ts b/plugins/copy-role-color/src/index.ts index 1ea1a72..ac2079f 100644 --- a/plugins/copy-role-color/src/index.ts +++ b/plugins/copy-role-color/src/index.ts @@ -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 diff --git a/plugins/custom-user-tags/src/index.ts b/plugins/custom-user-tags/src/index.ts index ddbd7c6..0f51659 100644 --- a/plugins/custom-user-tags/src/index.ts +++ b/plugins/custom-user-tags/src/index.ts @@ -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, { diff --git a/plugins/fake-profile-themes-and-effects/src/index.ts b/plugins/fake-profile-themes-and-effects/src/index.ts index 4e6498f..1d89abe 100644 --- a/plugins/fake-profile-themes-and-effects/src/index.ts +++ b/plugins/fake-profile-themes-and-effects/src/index.ts @@ -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. diff --git a/plugins/key-inspector/src/index.tsx b/plugins/key-inspector/src/index.tsx index a18fc20..8b685b1 100644 --- a/plugins/key-inspector/src/index.tsx +++ b/plugins/key-inspector/src/index.tsx @@ -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 }; diff --git a/plugins/message-logger/src/index.ts b/plugins/message-logger/src/index.ts index 7fd70aa..1ae6a1a 100644 --- a/plugins/message-logger/src/index.ts +++ b/plugins/message-logger/src/index.ts @@ -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"); diff --git a/plugins/message-snippets/src/index.ts b/plugins/message-snippets/src/index.ts index ea9caa6..cdd05b7 100644 --- a/plugins/message-snippets/src/index.ts +++ b/plugins/message-snippets/src/index.ts @@ -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()), diff --git a/plugins/more-alts/src/index.ts b/plugins/more-alts/src/index.ts index 0c5f9e7..4c8972b 100644 --- a/plugins/more-alts/src/index.ts +++ b/plugins/more-alts/src/index.ts @@ -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 { diff --git a/plugins/pronoun-db/src/index.tsx b/plugins/pronoun-db/src/index.tsx index 14fae36..dcef0c7 100644 --- a/plugins/pronoun-db/src/index.tsx +++ b/plugins/pronoun-db/src/index.tsx @@ -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; diff --git a/plugins/radial-status/src/index.ts b/plugins/radial-status/src/index.ts index 715e779..a7803b9 100644 --- a/plugins/radial-status/src/index.ts +++ b/plugins/radial-status/src/index.ts @@ -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) { diff --git a/plugins/reminders/src/index.ts b/plugins/reminders/src/index.ts index 273465f..beaf5fb 100644 --- a/plugins/reminders/src/index.ts +++ b/plugins/reminders/src/index.ts @@ -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(); }, diff --git a/plugins/reverse-image-search/src/index.ts b/plugins/reverse-image-search/src/index.ts index fb7f2e3..81daad4 100644 --- a/plugins/reverse-image-search/src/index.ts +++ b/plugins/reverse-image-search/src/index.ts @@ -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()) diff --git a/plugins/role-color-everywhere/src/index.ts b/plugins/role-color-everywhere/src/index.ts index 51e64e5..bf8cc10 100644 --- a/plugins/role-color-everywhere/src/index.ts +++ b/plugins/role-color-everywhere/src/index.ts @@ -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; diff --git a/plugins/rose-plugs/src/index.ts b/plugins/rose-plugs/src/index.ts index 2a6bf81..2217a43 100644 --- a/plugins/rose-plugs/src/index.ts +++ b/plugins/rose-plugs/src/index.ts @@ -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) { diff --git a/plugins/rose-utils/src/index.ts b/plugins/rose-utils/src/index.ts index 9219c08..f9e2075 100644 --- a/plugins/rose-utils/src/index.ts +++ b/plugins/rose-utils/src/index.ts @@ -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) { diff --git a/plugins/split-messages/src/index.ts b/plugins/split-messages/src/index.ts index 635ae3e..275ba77 100644 --- a/plugins/split-messages/src/index.ts +++ b/plugins/split-messages/src/index.ts @@ -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"); diff --git a/plugins/staff-tags/src/index.ts b/plugins/staff-tags/src/index.ts index 1473ccb..69631a8 100644 --- a/plugins/staff-tags/src/index.ts +++ b/plugins/staff-tags/src/index.ts @@ -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 ??= {}; diff --git a/plugins/typing-avatars/src/index.ts b/plugins/typing-avatars/src/index.ts index 67ed0e0..3014fac 100644 --- a/plugins/typing-avatars/src/index.ts +++ b/plugins/typing-avatars/src/index.ts @@ -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 }); diff --git a/plugins/urban-dictionary/src/index.ts b/plugins/urban-dictionary/src/index.ts index fb7f2e3..81daad4 100644 --- a/plugins/urban-dictionary/src/index.ts +++ b/plugins/urban-dictionary/src/index.ts @@ -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()) diff --git a/plugins/view-raw/src/index.tsx b/plugins/view-raw/src/index.tsx index 479086b..c616204 100644 --- a/plugins/view-raw/src/index.tsx +++ b/plugins/view-raw/src/index.tsx @@ -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({button}); + 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({button}); - 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, }; diff --git a/plugins/you-bar-plus/src/index.ts b/plugins/you-bar-plus/src/index.ts index c545aeb..8342dff 100644 --- a/plugins/you-bar-plus/src/index.ts +++ b/plugins/you-bar-plus/src/index.ts @@ -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; diff --git a/plugins/zipline/src/index.ts b/plugins/zipline/src/index.ts index c47b640..f7510eb 100644 --- a/plugins/zipline/src/index.ts +++ b/plugins/zipline/src/index.ts @@ -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, { diff --git a/serve.mjs b/serve.mjs new file mode 100644 index 0000000..6f2295d --- /dev/null +++ b/serve.mjs @@ -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 //manifest.json or //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 //install or //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`); +}); diff --git a/shared/lib/blacklist.ts b/shared/lib/blacklist.ts new file mode 100644 index 0000000..d44f61c --- /dev/null +++ b/shared/lib/blacklist.ts @@ -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([ + ...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; +} diff --git a/shared/lib/recordToGateway.ts b/shared/lib/recordToGateway.ts new file mode 100644 index 0000000..ff8cbcd --- /dev/null +++ b/shared/lib/recordToGateway.ts @@ -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, + }; +} diff --git a/shared/lib/utility.ts b/shared/lib/utility.ts new file mode 100644 index 0000000..b98ddb9 --- /dev/null +++ b/shared/lib/utility.ts @@ -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; +}