feat: port Antied & Antied Zero, add repository-wide blacklist enforcement
This commit is contained in:
parent
82eec74856
commit
d4f1332f5a
45 changed files with 1761 additions and 95 deletions
4
blacklist.json
Normal file
4
blacklist.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"updated_at": "2026-09-06T15:25:00.000Z",
|
||||
"list": []
|
||||
}
|
||||
|
|
@ -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 (_) {}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
12
plugins/antied-zero/manifest.json
Normal file
12
plugins/antied-zero/manifest.json
Normal 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"
|
||||
}
|
||||
}
|
||||
45
plugins/antied-zero/src/index.ts
Normal file
45
plugins/antied-zero/src/index.ts
Normal 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,
|
||||
};
|
||||
87
plugins/antied-zero/src/patches/actionSheet.tsx
Normal file
87
plugins/antied-zero/src/patches/actionSheet.tsx
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
106
plugins/antied-zero/src/patches/fluxDispatch.ts
Normal file
106
plugins/antied-zero/src/patches/fluxDispatch.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
23
plugins/antied-zero/src/patches/selfEdit.ts
Normal file
23
plugins/antied-zero/src/patches/selfEdit.ts
Normal 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];
|
||||
}
|
||||
});
|
||||
}
|
||||
16
plugins/antied-zero/src/ui/Settings.tsx
Normal file
16
plugins/antied-zero/src/ui/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
12
plugins/antied/manifest.json
Normal file
12
plugins/antied/manifest.json
Normal 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
148
plugins/antied/src/index.ts
Normal 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,
|
||||
};
|
||||
51
plugins/antied/src/lib/types.ts
Normal file
51
plugins/antied/src/lib/types.ts
Normal 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;
|
||||
}
|
||||
126
plugins/antied/src/patches/actionSheet.tsx
Normal file
126
plugins/antied/src/patches/actionSheet.tsx
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
71
plugins/antied/src/patches/contextMenu.ts
Normal file
71
plugins/antied/src/patches/contextMenu.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
175
plugins/antied/src/patches/fluxDispatch.ts
Normal file
175
plugins/antied/src/patches/fluxDispatch.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
40
plugins/antied/src/patches/messageRecord.ts
Normal file
40
plugins/antied/src/patches/messageRecord.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
30
plugins/antied/src/patches/selfEdit.ts
Normal file
30
plugins/antied/src/patches/selfEdit.ts
Normal 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];
|
||||
}
|
||||
});
|
||||
}
|
||||
114
plugins/antied/src/patches/updateRows.ts
Normal file
114
plugins/antied/src/patches/updateRows.ts
Normal 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();
|
||||
};
|
||||
}
|
||||
169
plugins/antied/src/ui/Settings.tsx
Normal file
169
plugins/antied/src/ui/Settings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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 ??= {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
112
serve.mjs
Normal 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
91
shared/lib/blacklist.ts
Normal 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;
|
||||
}
|
||||
85
shared/lib/recordToGateway.ts
Normal file
85
shared/lib/recordToGateway.ts
Normal 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
73
shared/lib/utility.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue