From 48662f828cceea1b33e91103dedc9c9871c18390 Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sun, 2 Aug 2026 11:46:04 -0400 Subject: [PATCH] RoseUtils: expand Developer Mode, add message/server dev-tools, drop Tenor fix --- .../rose-utils/src/lib/messageActionSheet.ts | 144 +++++++++++++++++ plugins/rose-utils/src/lib/snowflake.ts | 15 ++ .../src/modules/DeveloperMode/index.ts | 152 +++++++++++++++++ .../src/modules/EnforceDeveloperMode/index.ts | 45 ------ .../src/modules/MessageDevTools/index.ts | 153 ++++++++++++++++++ .../src/modules/ServerInfoTools/index.ts | 131 +++++++++++++++ .../src/modules/SnowflakeTools/index.ts | 50 ++++++ .../src/modules/TenorGifFix/dark.png | Bin 16339 -> 0 bytes .../src/modules/TenorGifFix/index.ts | 65 -------- .../src/modules/TenorGifFix/light.png | Bin 25131 -> 0 bytes plugins/rose-utils/src/modules/index.ts | 18 ++- 11 files changed, 660 insertions(+), 113 deletions(-) create mode 100644 plugins/rose-utils/src/lib/messageActionSheet.ts create mode 100644 plugins/rose-utils/src/lib/snowflake.ts create mode 100644 plugins/rose-utils/src/modules/DeveloperMode/index.ts delete mode 100644 plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts create mode 100644 plugins/rose-utils/src/modules/MessageDevTools/index.ts create mode 100644 plugins/rose-utils/src/modules/ServerInfoTools/index.ts create mode 100644 plugins/rose-utils/src/modules/SnowflakeTools/index.ts delete mode 100644 plugins/rose-utils/src/modules/TenorGifFix/dark.png delete mode 100644 plugins/rose-utils/src/modules/TenorGifFix/index.ts delete mode 100644 plugins/rose-utils/src/modules/TenorGifFix/light.png diff --git a/plugins/rose-utils/src/lib/messageActionSheet.ts b/plugins/rose-utils/src/lib/messageActionSheet.ts new file mode 100644 index 0000000..383c5e1 --- /dev/null +++ b/plugins/rose-utils/src/lib/messageActionSheet.ts @@ -0,0 +1,144 @@ +import { before, after } from "@vendetta/patcher"; +import { findByProps } from "@vendetta/metro"; +import { React } from "@vendetta/metro/common"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { findInReactTree } from "@vendetta/utils"; + +// Injection technique ported from this repo's ViewRaw plugin (view-raw/src/index.tsx) - confirmed +// working there for adding a row to the message long-press action sheet. Generalized here so more +// than one RoseUtils module can each contribute their own rows through a single shared patch, +// instead of every module that wants this hook patching openLazy separately (which would stack +// duplicate patches, and duplicate "instance.__patched" bookkeeping, the moment more than one such +// module is enabled at once). +export interface MessageActionRow { + key: string; + label: string; + sublabel?: string; + icon?: string; + onPress: (message: any) => void; +} + +export type MessageActionBuilder = (message: any) => MessageActionRow[]; + +const LazyActionSheet = findByProps("openLazy", "hideActionSheet"); +const { ActionSheetRow } = findByProps("ActionSheetRow") ?? {}; + +const builders = new Set(); +let realUnpatch: (() => void) | null = null; + +function buildRowElement(row: MessageActionRow, message: any) { + if (!ActionSheetRow) return null; + + return React.createElement(ActionSheetRow, { + label: row.label, + subLabel: row.sublabel, + icon: row.icon ? React.createElement(ActionSheetRow.Icon, { source: getAssetIDByName(row.icon) }) : undefined, + onPress: () => { + try { + row.onPress(message); + } finally { + LazyActionSheet?.hideActionSheet?.(); + } + }, + key: row.key, + }); +} + +function collectRowElements(message: any) { + const elements: any[] = []; + for (const build of builders) { + try { + for (const row of build(message)) { + const el = buildRowElement(row, message); + if (el) elements.push(el); + } + } catch { + // A broken builder shouldn't block anyone else's rows from showing up. + } + } + return elements; +} + +function patchOpenLazy(): () => void { + return before("openLazy", LazyActionSheet, ([component, key, msg]: any[]) => { + if (key !== "MessageLongPressActionSheet" || !msg?.message) return; + + component.then((instance: any) => { + // Discord appears to reuse the same lazy-loaded instance across every action sheet + // open - only patch it once, and track the active message on the instance itself so + // re-opening on a different message doesn't need a fresh patch. + instance.__roseUtilsActiveMessage = msg.message; + if (instance.__roseUtilsPatched) return; + instance.__roseUtilsPatched = true; + + after("default", instance, (_: any, result: any) => { + try { + const rows = collectRowElements(instance.__roseUtilsActiveMessage); + if (!rows.length) return; + + // Real ActionSheetRow groups - search every group for one that already has + // ActionSheetRow-shaped children and insert there, falling back to a brand new + // group at the top if none match. + const groups = findInReactTree( + result, + (x: any) => Array.isArray(x) && x[0]?.type?.name === "ActionSheetRowGroup", + ); + + if (Array.isArray(groups) && groups.length && ActionSheetRow) { + let inserted = false; + for (const group of groups) { + const children = findInReactTree( + group, + (c: any) => Array.isArray(c) && c.some((child: any) => child?.type?.name === "ActionSheetRow"), + ); + if (Array.isArray(children)) { + children.push(...rows); + inserted = true; + break; + } + } + if (!inserted && typeof groups.unshift === "function" && ActionSheetRow.Group) { + groups.unshift(React.createElement(ActionSheetRow.Group, {}, rows)); + inserted = true; + } + if (inserted) return; + } + + // Name-independent fallback: any array where every element already looks like + // an action sheet row (has both a label and an onPress). + const genericRowGroup = findInReactTree( + result, + (x: any) => + 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(...rows); + } catch { + // Best-effort - if the action sheet shape doesn't match any known strategy, + // the extra rows just don't appear rather than crashing the sheet. + } + }); + }); + }); +} + +/** + * Registers a row builder for the message long-press action sheet. Returns an unregister + * function. The underlying openLazy patch is reference-counted across every caller - it's only + * applied on the first registration and only removed once the last one unregisters, so modules + * can start/stop independently of each other's registration order. + */ +export function registerMessageAction(builder: MessageActionBuilder): () => void { + if (!realUnpatch && LazyActionSheet) realUnpatch = patchOpenLazy(); + builders.add(builder); + + return () => { + builders.delete(builder); + if (builders.size === 0 && realUnpatch) { + realUnpatch(); + realUnpatch = null; + } + }; +} diff --git a/plugins/rose-utils/src/lib/snowflake.ts b/plugins/rose-utils/src/lib/snowflake.ts new file mode 100644 index 0000000..cc38127 --- /dev/null +++ b/plugins/rose-utils/src/lib/snowflake.ts @@ -0,0 +1,15 @@ +// Pure math, no Discord internals involved - Discord IDs are Twitter-style snowflakes: the top 42 +// bits are milliseconds since the Discord epoch (2015-01-01T00:00:00.000Z). +const DISCORD_EPOCH = 1420070400000n; + +export function decodeSnowflakeTimestamp(id: string): Date | null { + const trimmed = id.trim(); + if (!/^\d{15,20}$/.test(trimmed)) return null; + + try { + const ms = (BigInt(trimmed) >> 22n) + DISCORD_EPOCH; + return new Date(Number(ms)); + } catch { + return null; + } +} diff --git a/plugins/rose-utils/src/modules/DeveloperMode/index.ts b/plugins/rose-utils/src/modules/DeveloperMode/index.ts new file mode 100644 index 0000000..5d66da3 --- /dev/null +++ b/plugins/rose-utils/src/modules/DeveloperMode/index.ts @@ -0,0 +1,152 @@ +import { findByProps, findByStoreName } from "@vendetta/metro"; +import { clipboard } from "@vendetta/metro/common"; +import { showToast } from "@vendetta/ui/toasts"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { fluxSubscribe } from "@shared/lib/flux"; +import { Module, ModuleCategory } from "../../lib/Module"; +import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet"; + +// Confirmed against decompiled current-build Discord source: Developer Mode is a proto-backed +// setting exposed as DeveloperMode.getSetting()/updateSetting(bool), part of the same +// modules/user_settings/UserSettings.tsx module that exports every other appearance/advanced +// setting (DarkSidebar, MessageDisplayCompact, etc - all built the same way via defineProtoSetting). +// It resets itself more than it should on Revenge - rather than guess at why, this just re-asserts +// it every time the user's settings proto changes, which is the same event Discord's own settings +// UI reacts to. +const UserSettingsModule = findByProps("DeveloperMode", "DarkSidebar"); + +// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/activity-payload-capture.txt): +// caught a real LOCAL_ACTIVITY_UPDATE payload while Watch Together was running - the activity +// object is exactly {type, name, application_id, platform, party, secrets, flags, metadata}, snake +// case, matching the primary branch checked below. That capture was for the local user specifically +// (own activities dispatch through a local-first path, which is why PresenceStore.getActivities on +// your own ID came back empty even mid-session) - but it's the same public Gateway Activity object +// schema Discord broadcasts for everyone, which is what PresenceStore caches for *other* users - +// the actual case this code needs, since it looks up the message author, not the local user. Not +// yet caught a live PresenceStore.getActivities(otherUserId) call directly, but the shape match +// leaves nothing left to guess at. +const PresenceStore = findByStoreName("PresenceStore"); + +// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against real +// slash-command messages: message.interaction is populated with {id, name, type, user, +// displayName} exactly as read below (message.type 20 = CHAT_INPUT_COMMAND on these). +// interactionMetadata/interaction_metadata are checked as a fallback for whichever build doesn't +// populate the legacy .interaction field. +function getCommandInteraction(message: any): { id: string; name?: string } | null { + const interaction = message?.interaction ?? message?.interactionMetadata ?? message?.interaction_metadata; + if (!interaction?.id) return null; + return { id: String(interaction.id), name: interaction.name }; +} + +function commandIdRows(message: any): MessageActionRow[] { + const interaction = getCommandInteraction(message); + if (!interaction) return []; + + return [ + { + key: "rose-utils-copy-command-id", + label: "Copy Command ID", + sublabel: interaction.name ? `/${interaction.name}` : "Slash command interaction", + icon: "ic_copy_24px", + onPress: () => { + clipboard.setString(interaction.id); + showToast("Copied command ID to clipboard", getAssetIDByName("ic_copy_24px")); + }, + }, + ]; +} + +function activityIdRows(message: any): MessageActionRow[] { + if (!PresenceStore?.getActivities) return []; + + const authorId = message?.author?.id; + if (!authorId) return []; + + let activities: any[]; + try { + activities = PresenceStore.getActivities(authorId); + } catch { + return []; + } + if (!Array.isArray(activities)) return []; + + const withAppId = activities.filter((a) => a?.application_id ?? a?.applicationId); + if (!withAppId.length) return []; + + return withAppId.map((activity, i) => { + const appId = String(activity.application_id ?? activity.applicationId); + return { + key: `rose-utils-copy-activity-id-${i}`, + label: withAppId.length > 1 ? `Copy Activity ID (${activity.name ?? i + 1})` : "Copy Activity/Game ID", + sublabel: activity.name, + icon: "ic_copy_24px", + onPress: () => { + clipboard.setString(appId); + showToast(`Copied ${activity.name ?? "activity"} ID to clipboard`, getAssetIDByName("ic_copy_24px")); + }, + }; + }); +} + +export default new Module({ + id: "enforce-developer-mode", + label: "Developer Mode", + meta: { + sublabel: "Turns on Discord's real Developer Mode (all its native Copy ID options) and adds extra copy-ID tools slash commands and activities don't get natively", + category: ModuleCategory.Useful, + }, + settings: { + keepNativeOn: { + label: "Keep native Developer Mode on", + subLabel: "Re-asserts Discord's own Developer Mode setting - it tends to reset itself on Revenge-based clients", + type: "toggle", + default: true, + }, + copyCommandId: { + label: "Copy Command ID", + subLabel: "Adds a row to the message long-press menu for messages sent via a slash command", + type: "toggle", + default: true, + }, + copyActivityId: { + label: "Copy Activity/Game ID", + subLabel: "Adds a row to the message long-press menu to copy the author's current game/activity application ID (not every activity type has one, e.g. a plain custom status)", + type: "toggle", + default: true, + }, + }, + handlers: { + onStart() { + if (this.storage.options.keepNativeOn) { + const DeveloperMode = UserSettingsModule?.DeveloperMode; + if (DeveloperMode?.getSetting && DeveloperMode?.updateSetting) { + const enforce = () => { + try { + if (!DeveloperMode.getSetting()) DeveloperMode.updateSetting(true); + } catch { + // Best-effort - if this particular check throws, the next settings change retries it. + } + }; + + enforce(); + this.patches.add(fluxSubscribe("USER_SETTINGS_PROTO_UPDATE", enforce)); + + // Belt and suspenders: if whatever resets this doesn't go through + // USER_SETTINGS_PROTO_UPDATE for some reason, this still catches it within 5s + // instead of staying off for the rest of the session. + const interval = setInterval(enforce, 5000); + this.patches.add(() => clearInterval(interval)); + } + } + + if (this.storage.options.copyCommandId) { + this.patches.add(registerMessageAction(commandIdRows)); + } + + if (this.storage.options.copyActivityId) { + this.patches.add(registerMessageAction(activityIdRows)); + } + }, + onStop() {}, + }, +}); diff --git a/plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts b/plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts deleted file mode 100644 index 911d230..0000000 --- a/plugins/rose-utils/src/modules/EnforceDeveloperMode/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { findByProps } from "@vendetta/metro"; -import { fluxSubscribe } from "@shared/lib/flux"; -import { Module, ModuleCategory } from "../../lib/Module"; - -// Confirmed against decompiled current-build Discord source: Developer Mode is a proto-backed -// setting exposed as DeveloperMode.getSetting()/updateSetting(bool), part of the same -// modules/user_settings/UserSettings.tsx module that exports every other appearance/advanced -// setting (DarkSidebar, MessageDisplayCompact, etc - all built the same way via defineProtoSetting). -// It resets itself more than it should on Revenge - rather than guess at why, this just re-asserts -// it every time the user's settings proto changes, which is the same event Discord's own settings -// UI reacts to. -const UserSettingsModule = findByProps("DeveloperMode", "DarkSidebar"); - -export default new Module({ - id: "enforce-developer-mode", - label: "Enforce Developer Mode", - meta: { - sublabel: "Keeps Developer Mode switched on - it tends to reset itself on Revenge-based clients", - category: ModuleCategory.Fixes, - }, - handlers: { - onStart() { - const DeveloperMode = UserSettingsModule?.DeveloperMode; - if (!DeveloperMode?.getSetting || !DeveloperMode?.updateSetting) return; - - const enforce = () => { - try { - if (!DeveloperMode.getSetting()) DeveloperMode.updateSetting(true); - } catch { - // Best-effort - if this particular check throws, the next settings change retries it. - } - }; - - enforce(); - this.patches.add(fluxSubscribe("USER_SETTINGS_PROTO_UPDATE", enforce)); - - // Belt and suspenders: if whatever resets this doesn't go through - // USER_SETTINGS_PROTO_UPDATE for some reason, this still catches it within 5s instead - // of staying off for the rest of the session. - const interval = setInterval(enforce, 5000); - this.patches.add(() => clearInterval(interval)); - }, - onStop() {}, - }, -}); diff --git a/plugins/rose-utils/src/modules/MessageDevTools/index.ts b/plugins/rose-utils/src/modules/MessageDevTools/index.ts new file mode 100644 index 0000000..5d11994 --- /dev/null +++ b/plugins/rose-utils/src/modules/MessageDevTools/index.ts @@ -0,0 +1,153 @@ +import { clipboard } from "@vendetta/metro/common"; +import { showToast } from "@vendetta/ui/toasts"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { Module, ModuleCategory } from "../../lib/Module"; +import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet"; + +const COPY_ICON = "ic_copy_24px"; + +function copy(value: string, toastLabel: string) { + clipboard.setString(value); + showToast(toastLabel, getAssetIDByName(COPY_ICON)); +} + +// Every field read below (webhookId, nonce, attachments, messageReference, type/flags/timestamps) +// is part of Discord's own public message object schema - the same shape the client already +// deserializes the gateway/REST payload into before handing it to the action sheet - not +// internal-UI guesswork, unlike the activity-ID lookup in the Developer Mode module. webhookId, +// nonce, and messageReference are confirmed live via Key Inspector Eval +// (/root/eval-for-revenge/ru/all-checks.txt) against real interaction-response and reply messages. +function webhookIdRow(message: any): MessageActionRow[] { + const webhookId = message?.webhookId ?? message?.webhook_id; + if (!webhookId) return []; + + return [ + { + key: "rose-utils-copy-webhook-id", + label: "Copy Webhook ID", + icon: COPY_ICON, + onPress: () => copy(String(webhookId), "Copied webhook ID to clipboard"), + }, + ]; +} + +function nonceRow(message: any): MessageActionRow[] { + const nonce = message?.nonce; + if (!nonce) return []; + + return [ + { + key: "rose-utils-copy-nonce", + label: "Copy Nonce", + icon: COPY_ICON, + onPress: () => copy(String(nonce), "Copied nonce to clipboard"), + }, + ]; +} + +function attachmentIdsRow(message: any): MessageActionRow[] { + const attachments = message?.attachments; + if (!Array.isArray(attachments) || !attachments.length) return []; + + const ids = attachments.map((a: any) => a?.id).filter(Boolean); + if (!ids.length) return []; + + return [ + { + key: "rose-utils-copy-attachment-ids", + label: ids.length > 1 ? `Copy Attachment IDs (${ids.length})` : "Copy Attachment ID", + icon: COPY_ICON, + onPress: () => copy(ids.join(", "), `Copied ${ids.length} attachment ID${ids.length === 1 ? "" : "s"} to clipboard`), + }, + ]; +} + +function referencedMessageIdRow(message: any): MessageActionRow[] { + const ref = message?.messageReference ?? message?.message_reference; + const refId = ref?.messageId ?? ref?.message_id; + if (!refId) return []; + + return [ + { + key: "rose-utils-copy-referenced-id", + label: "Copy Replied-to Message ID", + icon: COPY_ICON, + onPress: () => copy(String(refId), "Copied replied-to message ID to clipboard"), + }, + ]; +} + +function debugInfoRow(message: any): MessageActionRow[] { + if (!message?.id) return []; + + return [ + { + key: "rose-utils-copy-debug-info", + label: "Copy Message Debug Info", + sublabel: "Type, flags, and timestamps", + icon: COPY_ICON, + onPress: () => { + const info = { + id: message.id, + channelId: message.channelId ?? message.channel_id, + type: message.type, + flags: message.flags, + timestamp: message.timestamp, + editedTimestamp: message.editedTimestamp ?? message.edited_timestamp, + }; + copy(JSON.stringify(info, null, 2), "Copied message debug info to clipboard"); + }, + }, + ]; +} + +export default new Module({ + id: "message-dev-tools", + label: "Message Dev Tools", + meta: { + sublabel: "Extra copy-ID rows in the message long-press menu, beyond what Discord's Developer Mode gives you natively", + category: ModuleCategory.Useful, + }, + settings: { + copyWebhookId: { + label: "Copy Webhook ID", + subLabel: "For messages sent by a webhook", + type: "toggle", + default: true, + }, + copyNonce: { + label: "Copy Nonce", + subLabel: "The client-generated ID used to match an optimistic send to its real message", + type: "toggle", + default: true, + }, + copyAttachmentIds: { + label: "Copy Attachment ID(s)", + type: "toggle", + default: true, + }, + copyReferencedId: { + label: "Copy Replied-to Message ID", + type: "toggle", + default: true, + }, + copyDebugInfo: { + label: "Copy Message Debug Info", + subLabel: "Type, flags, and creation/edit timestamps as JSON", + type: "toggle", + default: true, + }, + }, + handlers: { + onStart() { + const options = this.storage.options; + + if (options.copyWebhookId) this.patches.add(registerMessageAction(webhookIdRow)); + if (options.copyNonce) this.patches.add(registerMessageAction(nonceRow)); + if (options.copyAttachmentIds) this.patches.add(registerMessageAction(attachmentIdsRow)); + if (options.copyReferencedId) this.patches.add(registerMessageAction(referencedMessageIdRow)); + if (options.copyDebugInfo) this.patches.add(registerMessageAction(debugInfoRow)); + }, + onStop() {}, + }, +}); diff --git a/plugins/rose-utils/src/modules/ServerInfoTools/index.ts b/plugins/rose-utils/src/modules/ServerInfoTools/index.ts new file mode 100644 index 0000000..3ab3fd3 --- /dev/null +++ b/plugins/rose-utils/src/modules/ServerInfoTools/index.ts @@ -0,0 +1,131 @@ +import { findByStoreName } from "@vendetta/metro"; +import { clipboard } from "@vendetta/metro/common"; +import { showToast } from "@vendetta/ui/toasts"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { Module, ModuleCategory } from "../../lib/Module"; +import { registerMessageAction, type MessageActionRow } from "../../lib/messageActionSheet"; +import { decodeSnowflakeTimestamp } from "../../lib/snowflake"; + +// Confirmed live via Key Inspector Eval (/root/eval-for-revenge/ru/all-checks.txt) against a real +// server: GuildStore.getGuild() returns name/ownerId/features/premiumTier/premiumSubscriberCount/ +// verificationLevel/vanityURLCode exactly as read below. RelationshipStore.getFriendIDs() and +// GuildMemberStore.getMember(guildId, userId) are confirmed working too - a real friend showed up +// as a cached member of the test server, cross-checked against UserStore.getUser().username. +const GuildStore = findByStoreName("GuildStore"); +const RelationshipStore = findByStoreName("RelationshipStore"); +const GuildMemberStore = findByStoreName("GuildMemberStore"); +const UserStore = findByStoreName("UserStore"); + +const COPY_ICON = "ic_copy_24px"; + +function copy(value: string, toastLabel: string) { + clipboard.setString(value); + showToast(toastLabel, getAssetIDByName(COPY_ICON)); +} + +function getGuildId(message: any): string | null { + return message?.guildId ?? message?.guild_id ?? null; +} + +function serverInfoRow(message: any): MessageActionRow[] { + const guildId = getGuildId(message); + if (!guildId || !GuildStore?.getGuild) return []; + + const guild = GuildStore.getGuild(guildId); + if (!guild) return []; + + return [ + { + key: "rose-utils-copy-server-info", + label: "Copy Server Info", + sublabel: guild.name, + icon: COPY_ICON, + onPress: () => { + const info = { + id: guildId, + name: guild.name, + ownerId: guild.ownerId, + createdAt: decodeSnowflakeTimestamp(guildId)?.toISOString(), + boostTier: guild.premiumTier, + boostCount: guild.premiumSubscriberCount, + verificationLevel: guild.verificationLevel, + vanityURLCode: guild.vanityURLCode, + description: guild.description, + features: guild.features, + }; + copy(JSON.stringify(info, null, 2), "Copied server info to clipboard"); + }, + }, + ]; +} + +function friendsInServerRow(message: any): MessageActionRow[] { + const guildId = getGuildId(message); + if (!guildId || !RelationshipStore?.getFriendIDs || !GuildMemberStore?.getMember) return []; + + let friendIds: string[]; + try { + friendIds = RelationshipStore.getFriendIDs(); + } catch { + return []; + } + if (!Array.isArray(friendIds) || !friendIds.length) return []; + + return [ + { + key: "rose-utils-copy-friends-in-server", + label: "Copy Friends In This Server", + sublabel: "Only counts friends already loaded into this server's member list this session", + icon: COPY_ICON, + onPress: () => { + const inServer = friendIds + .filter((id) => { + try { + return !!GuildMemberStore.getMember(guildId, id); + } catch { + return false; + } + }) + .map((id) => UserStore?.getUser?.(id)?.username ?? id); + + copy( + inServer.length + ? inServer.join(", ") + : "(none found - they may just not be loaded into the member list yet)", + `Copied ${inServer.length} friend${inServer.length === 1 ? "" : "s"} in this server to clipboard`, + ); + }, + }, + ]; +} + +export default new Module({ + id: "server-info-tools", + label: "Server Info Tools", + meta: { + sublabel: "Adds Copy Server Info and Copy Friends In This Server rows to the message long-press menu", + category: ModuleCategory.Useful, + }, + settings: { + copyServerInfo: { + label: "Copy Server Info", + subLabel: "Boost tier, verification level, features, vanity URL, creation date, and more", + type: "toggle", + default: true, + }, + copyFriendsInServer: { + label: "Copy Friends In This Server", + subLabel: "Cross-references your friends list against this server's cached member list", + type: "toggle", + default: true, + }, + }, + handlers: { + onStart() { + const options = this.storage.options; + if (options.copyServerInfo) this.patches.add(registerMessageAction(serverInfoRow)); + if (options.copyFriendsInServer) this.patches.add(registerMessageAction(friendsInServerRow)); + }, + onStop() {}, + }, +}); diff --git a/plugins/rose-utils/src/modules/SnowflakeTools/index.ts b/plugins/rose-utils/src/modules/SnowflakeTools/index.ts new file mode 100644 index 0000000..5303560 --- /dev/null +++ b/plugins/rose-utils/src/modules/SnowflakeTools/index.ts @@ -0,0 +1,50 @@ +import { clipboard } from "@vendetta/metro/common"; +import { showToast } from "@vendetta/ui/toasts"; +import { getAssetIDByName } from "@vendetta/ui/assets"; +import { Module, ModuleCategory } from "../../lib/Module"; +import { decodeSnowflakeTimestamp } from "../../lib/snowflake"; + +function decodeSnowflake(id: string): string | null { + return decodeSnowflakeTimestamp(id)?.toISOString() ?? null; +} + +export default new Module({ + id: "snowflake-tools", + label: "Snowflake Tools", + meta: { + sublabel: "Decode any Discord ID's creation timestamp - useful after copying an ID with Developer Mode", + category: ModuleCategory.Useful, + }, + settings: { + decodeClipboard: { + label: "Decode ID(s) in clipboard", + subLabel: "Copy one or more Discord IDs first (one per line, or comma-separated), then tap this", + type: "button", + action() { + clipboard.getString().then((text: string) => { + const ids = text.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean); + if (!ids.length) { + showToast("Clipboard is empty", undefined); + return; + } + + const lines = ids.map((id) => { + const decoded = decodeSnowflake(id); + return decoded ? `${id} -> ${decoded}` : `${id} -> not a valid snowflake`; + }); + + const report = lines.join("\n"); + clipboard.setString(report); + showToast( + `Decoded ${lines.length} ID${lines.length === 1 ? "" : "s"} - result copied`, + getAssetIDByName("ic_copy_24px"), + ); + }); + }, + }, + }, + handlers: { + onStart() {}, + onStop() {}, + }, +}); diff --git a/plugins/rose-utils/src/modules/TenorGifFix/dark.png b/plugins/rose-utils/src/modules/TenorGifFix/dark.png deleted file mode 100644 index 5fba546df0a6688f0853af45648606976751e74d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16339 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 (A6yAV0=1Ta)LjZ&`5dmxcj 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 diff --git a/plugins/rose-utils/src/modules/TenorGifFix/index.ts b/plugins/rose-utils/src/modules/TenorGifFix/index.ts deleted file mode 100644 index 65e4416..0000000 --- a/plugins/rose-utils/src/modules/TenorGifFix/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { findByProps } from "@vendetta/metro"; -import { before } from "@vendetta/patcher"; -import dark from "./dark.png"; -import light from "./light.png"; - -import { Module, ModuleCategory } from "../../lib/Module"; - -const MediaManager = findByProps("downloadMediaAsset"); -const mediaShareActions = findByProps("useMediaShareActions"); - -function parseURL(url: string) { - const path = url.split("/"); - const tenorIndex = path.findIndex((x) => x.endsWith(".tenor.com")); - if (tenorIndex === -1) return; - - const [host, id, file] = path.slice(tenorIndex, tenorIndex + 3); - if (!host || !id || !file) return; - - return `https://${host}/${id.slice(0, -2)}AC/${file.split(".")[0]}.gif`; -} - -export default new Module({ - id: "tenor-gif-fix", - label: "Tenor GIF Fix", - meta: { - sublabel: "Fixes a several-year old bug which downloads gifs from Tenor as videos instead of gif files", - category: ModuleCategory.Fixes, - }, - handlers: { - onStart() { - // Older Discord versions. - if (MediaManager?.downloadMediaAsset) { - this.patches.add( - before("downloadMediaAsset", MediaManager, (args: any[]) => { - const url = args[0]; - if (!url || typeof url !== "string") return; - - const parsed = parseURL(url); - if (parsed) { - args[0] = parsed; - args[1] = 1; - } - }), - ); - } - - // Newer Discord versions. - if (mediaShareActions?.useMediaShareActions) { - this.patches.add( - before("useMediaShareActions", mediaShareActions, (args: any[]) => { - const source = args[0]?.source; - if (!source?.uri) return; - - const parsed = parseURL(source.uri); - if (parsed) { - source.sourceURI = parsed; - delete source.videoURI; - } - }), - ); - } - }, - onStop() {}, - }, -}); diff --git a/plugins/rose-utils/src/modules/TenorGifFix/light.png b/plugins/rose-utils/src/modules/TenorGifFix/light.png deleted file mode 100644 index 421f14d3b886431c4c096e864c7ed9f4f880be41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25131 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 LmWE~eHzNKAL_&S7 diff --git a/plugins/rose-utils/src/modules/index.ts b/plugins/rose-utils/src/modules/index.ts index 4448ae9..e922a5c 100644 --- a/plugins/rose-utils/src/modules/index.ts +++ b/plugins/rose-utils/src/modules/index.ts @@ -1,11 +1,23 @@ import SpotifyListenAlong from "./SpotifyListenAlong"; -import TenorGifFix from "./TenorGifFix"; import SendSpotifyInvite from "./SendSpotifyInvite"; import Minimod from "./Minimod"; import ColorfulChannels from "./ColorfulChannels"; -import EnforceDeveloperMode from "./EnforceDeveloperMode"; +import DeveloperMode from "./DeveloperMode"; +import MessageDevTools from "./MessageDevTools"; +import SnowflakeTools from "./SnowflakeTools"; +import ServerInfoTools from "./ServerInfoTools"; // BetterComponents (Material-style Switch/Toast/Alert redesign) isn't ported - it needs // react-native-reanimated, which isn't part of this build, and it was already disabled by // default upstream due to known issues ("the great component functionification of 2025"). -export default [SpotifyListenAlong, TenorGifFix, SendSpotifyInvite, Minimod, ColorfulChannels, EnforceDeveloperMode]; +// TenorGifFix was dropped from this module list at the user's request. +export default [ + SpotifyListenAlong, + SendSpotifyInvite, + Minimod, + ColorfulChannels, + DeveloperMode, + MessageDevTools, + SnowflakeTools, + ServerInfoTools, +];