From 57ebbe1fd4ef158a47218a9364414d5895d10dc5 Mon Sep 17 00:00:00 2001 From: creations Date: Mon, 27 Oct 2025 14:14:15 -0400 Subject: [PATCH] Equibop supportHelper (#470) * Add support for equibop git hashes, Equicord/Equibop/commit/154ad1095b358e92a1f57f77ffcc38d691d1075a * Add support for equibop arrpc-bun debugging * move to an actual plugin for equibop instead of webrichp * move plugin to core, move arrp debug out of support helper * fix devbuild detection for equibop * Tidy * ARRPCBUN CHAT * WOW --------- Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com> --- src/equicordplugins/arRPCBun.web/index.tsx | 230 ++++++++++++++++++ src/equicordplugins/autoZipper/index.ts | 2 +- src/equicordplugins/equicordHelper/utils.tsx | 28 +++ .../instantScreenshare/utils.tsx | 1 + src/plugins/_core/supportHelper.tsx | 41 +--- src/plugins/arRPC.web/index.tsx | 9 +- src/utils/misc.ts | 12 +- 7 files changed, 281 insertions(+), 42 deletions(-) create mode 100644 src/equicordplugins/arRPCBun.web/index.tsx diff --git a/src/equicordplugins/arRPCBun.web/index.tsx b/src/equicordplugins/arRPCBun.web/index.tsx new file mode 100644 index 000000000..012c4f45d --- /dev/null +++ b/src/equicordplugins/arRPCBun.web/index.tsx @@ -0,0 +1,230 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { popNotice, showNotice } from "@api/Notices"; +import { definePluginSettings } from "@api/Settings"; +import { EquicordDevs } from "@utils/constants"; +import { Logger } from "@utils/Logger"; +import { isAnyPluginDev, isEquicordGuild } from "@utils/misc"; +import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; +import { findByCodeLazy } from "@webpack"; +import { ApplicationAssetUtils, FluxDispatcher, Toasts, UserStore } from "@webpack/common"; + +const fetchApplicationsRPC = findByCodeLazy('"Invalid Origin"', ".application"); +const logger = new Logger("arRPCBun"); + +async function lookupAsset(applicationId: string, key: string): Promise { + return (await ApplicationAssetUtils.fetchAssetIds(applicationId, [key]))[0]; +} + +const apps: any = {}; +async function lookupApp(applicationId: string): Promise { + const socket: any = {}; + await fetchApplicationsRPC(socket, applicationId); + return socket.application; +} + +let ws: WebSocket; + +export const settings = definePluginSettings({ + oldVerNotice: { + type: OptionType.BOOLEAN, + description: "old version notice for why arrpc bun doesnt work", + default: false, + hidden: true + }, + oneTimeNotice: { + type: OptionType.BOOLEAN, + description: "One time notice for showing arrpc disabled", + default: false, + hidden: true + }, +}); + + +export default definePlugin({ + name: "arRPCBun", + description: "arRPCBun integration", + authors: [EquicordDevs.creations], + reporterTestable: ReporterTestable.None, + enabledByDefault: IS_EQUIBOP, + settings, + + commands: [ + { + name: "arrpc-debug", + description: "Show arRPCBun debug information", + predicate: ctx => { + const result = isAnyPluginDev(UserStore.getCurrentUser()?.id) || isEquicordGuild(ctx?.guild?.id, true); + return result; + }, + execute: () => { + const arrpcStatus = IS_EQUIBOP ? VesktopNative.arrpc?.getStatus?.() : null; + + let content = ""; + + if (IS_EQUIBOP) { + const version = VesktopNative.app.getVersion(); + const gitHash = VesktopNative.app.getGitHash?.(); + const shortHash = gitHash?.slice(0, 7); + + content += `Equibop: v${version}`; + if (shortHash) { + content += ` • [${shortHash}]()`; + } + content += "\n"; + } + + if (arrpcStatus) { + content += `Running: ${arrpcStatus.running ? "Yes" : "No"}\n`; + content += `Enabled: ${arrpcStatus.enabled ? "Yes" : "No"}\n`; + + if (arrpcStatus.running) { + content += `Port: ${arrpcStatus.port}\n`; + content += `PID: ${arrpcStatus.pid}\n`; + + if (arrpcStatus.uptime) { + const seconds = Math.floor(arrpcStatus.uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + if (hours > 0) { + content += `Uptime: ${hours}h ${minutes % 60}m ${seconds % 60}s\n`; + } else if (minutes > 0) { + content += `Uptime: ${minutes}m ${seconds % 60}s\n`; + } else { + content += `Uptime: ${seconds}s\n`; + } + } + } + + const info = [ + arrpcStatus.restartCount > 0 && ["Restarts", arrpcStatus.restartCount], + arrpcStatus.bunPath && ["Bun", arrpcStatus.bunPath], + arrpcStatus.warnings?.length && ["Warnings", arrpcStatus.warnings.join(", ")], + arrpcStatus.lastError && ["Last Error", arrpcStatus.lastError], + arrpcStatus.lastExitCode && arrpcStatus.lastExitCode !== 0 && ["Exit Code", arrpcStatus.lastExitCode] + ].filter(Boolean); + + content += info.map(([type, value]) => `${type}: ${value}`).join("\n"); + } else { + if (ws) { + content += ws.readyState === WebSocket.OPEN + ? "WebSocket: Connected to external arRPCBun server\n" + : `WebSocket: ${["Connecting", "Open", "Closing", "Closed"][ws.readyState]}\n`; + } else { + content += "WebSocket: Not connected\n"; + } + } + + return { content }; + }, + }, + ], + + async handleEvent(e: MessageEvent) { + const data = JSON.parse(e.data); + + const { activity } = data; + const assets = activity?.assets; + + if (assets?.large_image) assets.large_image = await lookupAsset(activity.application_id, assets.large_image); + if (assets?.small_image) assets.small_image = await lookupAsset(activity.application_id, assets.small_image); + + if (activity) { + const appId = activity.application_id; + apps[appId] ||= await lookupApp(appId); + + const app = apps[appId]; + activity.name ||= app.name; + } + + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", ...data }); + }, + + async start() { + // only works on 3.0.8+ + if (IS_EQUIBOP) { + const version = VesktopNative.app.getVersion(); + const [major, minor, patch] = version.split(".").map(Number); + + if (major < 3 || (major === 3 && minor === 0 && patch < 8) && !settings.store.oldVerNotice) { + logger.error(`Equibop ${version} is too old. Requires 3.0.8+ for arRPCBun fix.`); + showNotice(`arRPCBun requires Equibop 3.0.8+. You have ${version}. Update Equibop to use this plugin.`, "OK", () => { + settings.store.oldVerNotice = true; + popNotice(); + }); + return; + } + } + + // disable WebRichPresence to avoid conflicts + const webRPC = Vencord.Plugins.plugins.WebRichPresence; + if (webRPC && Vencord.Plugins.isPluginEnabled("WebRichPresence")) { + logger.info("Disabling WebRichPresence to avoid conflicts"); + Vencord.Plugins.stopPlugin(webRPC); + } + + // get arRPC status from Equibop if available, otherwise use defaults + const arrpcStatus = IS_EQUIBOP ? VesktopNative.arrpc?.getStatus?.() : null; + + // if on Equibop and arRPC is disabled AND not running, warn user + if (IS_EQUIBOP && !arrpcStatus?.enabled && !arrpcStatus?.running && !settings.store.oneTimeNotice) { + logger.warn("Equibop's built-in arRPC is disabled and not running"); + showNotice("arRPC is not running. Enable it in Equibop settings, or run your own arRPCBun server.", "OK", () => { + settings.store.oneTimeNotice = true; + popNotice(); + }); + return; + } + + const host = arrpcStatus?.host || "127.0.0.1"; + const port = arrpcStatus?.port || 1337; + + const wsUrl = `ws://${host}:${port}`; + logger.info(`Connecting to arRPCBun at ${wsUrl}${arrpcStatus?.host ? "" : " (using defaults)"}`); + + if (ws) ws.close(); + ws = new WebSocket(wsUrl); + + ws.onmessage = this.handleEvent; + + ws.onerror = error => { + logger.error("WebSocket error:", error); + }; + + ws.onclose = () => { + logger.info("WebSocket closed"); + }; + + const connectionSuccessful = await new Promise(res => setTimeout(() => res(ws.readyState === WebSocket.OPEN), 5000)); + if (!connectionSuccessful) { + logger.error("Failed to connect to arRPCBun"); + showNotice("Failed to connect to arRPCBun, is it running?", "Retry", () => { + popNotice(); + this.start(); + }); + return; + } + + logger.info("Successfully connected to arRPCBun"); + Toasts.show({ + message: "Connected to arRPCBun", + type: Toasts.Type.SUCCESS, + id: Toasts.genId(), + options: { + duration: 1000, + position: Toasts.Position.BOTTOM + } + }); + }, + + stop() { + FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null }); + ws?.close(); + logger.info("Stopped arRPCBun connection"); + } +}); diff --git a/src/equicordplugins/autoZipper/index.ts b/src/equicordplugins/autoZipper/index.ts index 692084fad..ab0f2335f 100644 --- a/src/equicordplugins/autoZipper/index.ts +++ b/src/equicordplugins/autoZipper/index.ts @@ -75,7 +75,7 @@ async function readDirectoryEntry(entry: FileSystemDirectoryEntry): Promise => { return new Promise((resolve, reject) => { - reader.readEntries(async (entries) => { + reader.readEntries(async entries => { if (entries.length === 0) { resolve(); return; diff --git a/src/equicordplugins/equicordHelper/utils.tsx b/src/equicordplugins/equicordHelper/utils.tsx index 5d67bd929..57d1f072a 100644 --- a/src/equicordplugins/equicordHelper/utils.tsx +++ b/src/equicordplugins/equicordHelper/utils.tsx @@ -100,3 +100,31 @@ export async function toggleEnabled(name: string) { settings.enabled = !wasEnabled; return await beforeReturn(settings, wasEnabled); } + +export function getWindowsName(release: string) { + const build = parseInt(release.split(".")[2]); + if (build >= 22000) return "Windows 11"; + if (build >= 10240) return "Windows 10"; + if (build >= 9200) return "Windows 8.1"; + if (build >= 7600) return "Windows 7"; + return `Windows (${release})`; +} + +export function getMacOSName(release: string) { + const major = parseInt(release.split(".")[0]); + if (major === 24) return "MacOS 15 (Sequoia)"; + if (major === 23) return "MacOS 14 (Sonoma)"; + if (major === 22) return "MacOS 13 (Ventura)"; + if (major === 21) return "MacOS 12 (Monterey)"; + if (major === 20) return "MacOS 11 (Big Sur)"; + if (major === 19) return "MacOS 10.15 (Catalina)"; + return `MacOS (${release})`; +} + +export function platformName() { + if (typeof DiscordNative === "undefined") return navigator.platform; + if (DiscordNative.process.platform === "win32") return `${getWindowsName(DiscordNative.os.release)}`; + if (DiscordNative.process.platform === "darwin") return `${getMacOSName(DiscordNative.os.release)} (${DiscordNative.process.arch === "arm64" ? "Apple Silicon" : "Intel Silicon"})`; + if (DiscordNative.process.platform === "linux") return `Linux (${DiscordNative.os.release})`; + return DiscordNative.process.platform; +} diff --git a/src/equicordplugins/instantScreenshare/utils.tsx b/src/equicordplugins/instantScreenshare/utils.tsx index b73bc5ce7..3b0dc6a63 100644 --- a/src/equicordplugins/instantScreenshare/utils.tsx +++ b/src/equicordplugins/instantScreenshare/utils.tsx @@ -20,6 +20,7 @@ interface PickerProps { const getDesktopSources = findByCodeLazy("desktop sources"); const configModule = findByPropsLazy("getOutputVolume"); const log = new Logger("InstantScreenShare"); + export const settings = definePluginSettings({ streamMedia: { type: OptionType.COMPONENT, diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx index 030645ae0..873f621ee 100644 --- a/src/plugins/_core/supportHelper.tsx +++ b/src/plugins/_core/supportHelper.tsx @@ -25,6 +25,7 @@ import { Flex } from "@components/Flex"; import { Link } from "@components/Link"; import { Paragraph } from "@components/Paragraph"; import { openUpdaterModal } from "@components/settings/tabs/updater"; +import { platformName } from "@equicordplugins/equicordHelper/utils"; import { CONTRIB_ROLE_ID, Devs, DONOR_ROLE_ID, EQUIBOP_CONTRIB_ROLE_ID, EQUICORD_TEAM, GUILD_ID, SUPPORT_CHANNEL_ID, SUPPORT_CHANNEL_IDS, VC_CONTRIB_ROLE_ID, VC_DONOR_ROLE_ID, VC_GUILD_ID, VC_REGULAR_ROLE_ID, VC_SUPPORT_CHANNEL_IDS, VENCORD_CONTRIB_ROLE_ID } from "@utils/constants"; import { sendMessage } from "@utils/discord"; import { Logger } from "@utils/Logger"; @@ -71,41 +72,20 @@ async function forceUpdate() { return outdated; } -function getWindowsName(release: string) { - const build = parseInt(release.split(".")[2]); - if (build >= 22000) return "Windows 11"; - if (build >= 10240) return "Windows 10"; - if (build >= 9200) return "Windows 8.1"; - if (build >= 7600) return "Windows 7"; - return `Windows (${release})`; -} - -function getMacOSName(release: string) { - const major = parseInt(release.split(".")[0]); - if (major === 24) return "MacOS 15 (Sequoia)"; - if (major === 23) return "MacOS 14 (Sonoma)"; - if (major === 22) return "MacOS 13 (Ventura)"; - if (major === 21) return "MacOS 12 (Monterey)"; - if (major === 20) return "MacOS 11 (Big Sur)"; - if (major === 19) return "MacOS 10.15 (Catalina)"; - return `MacOS (${release})`; -} - -function platformName() { - if (typeof DiscordNative === "undefined") return navigator.platform; - if (DiscordNative.process.platform === "win32") return `${getWindowsName(DiscordNative.os.release)}`; - if (DiscordNative.process.platform === "darwin") return `${getMacOSName(DiscordNative.os.release)} (${DiscordNative.process.arch === "arm64" ? "Apple Silicon" : "Intel Silicon"})`; - if (DiscordNative.process.platform === "linux") return `Linux (${DiscordNative.os.release})`; - return DiscordNative.process.platform; -} - async function generateDebugInfoMessage() { const { RELEASE_CHANNEL } = window.GLOBAL_ENV; const client = (() => { if (IS_DISCORD_DESKTOP) return `Discord Desktop v${DiscordNative.app.getVersion()}`; if (IS_VESKTOP) return `Vesktop v${VesktopNative.app.getVersion()}`; - if (IS_EQUIBOP) return `Equibop v${VesktopNative.app.getVersion()}`; + if (IS_EQUIBOP) { + const equibopGitHash = tryOrElse(() => VesktopNative.app.getGitHash?.(), null); + if (equibopGitHash) { + const shortHash = equibopGitHash.slice(0, 7); + return `Equibop v${VesktopNative.app.getVersion()} • [${shortHash}]()`; + } + return `Equibop v${VesktopNative.app.getVersion()}`; + } if ("legcord" in window) return `LegCord v${window.legcord.version}`; // @ts-expect-error @@ -116,7 +96,7 @@ async function generateDebugInfoMessage() { const info = { Equicord: `v${VERSION} • [${shortGitHash()}]()` + - `${SettingsPlugin.getVersionInfo()} - ${Intl.DateTimeFormat("en-GB", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, + `${IS_EQUIBOP ? "" : SettingsPlugin.getVersionInfo()} - ${Intl.DateTimeFormat("en-US", { dateStyle: "medium" }).format(BUILD_TIMESTAMP)}`, Client: `${RELEASE_CHANNEL} ~ ${client}`, Platform: platformName() }; @@ -138,6 +118,7 @@ async function generateDebugInfoMessage() { "Activity Sharing Disabled": tryOrElse(() => !ShowCurrentGame.getSetting(), false), "Link Embeds Disabled": tryOrElse(() => !ShowEmbeds.getSetting(), false), "Equicord DevBuild": !IS_STANDALONE, + "Equibop DevBuild": IS_EQUIBOP && tryOrElse(() => VesktopNative.app.isDevBuild?.(), false), "Has UserPlugins": Object.values(PluginMeta).some(m => m.userPlugin), ">2 Weeks Outdated": BUILD_TIMESTAMP < Date.now() - 12096e5, [`Potentially Problematic Plugins: ${potentiallyProblematicPlugins.join(", ")}`]: potentiallyProblematicPlugins.length diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx index 07ced0878..9f4cf8d55 100644 --- a/src/plugins/arRPC.web/index.tsx +++ b/src/plugins/arRPC.web/index.tsx @@ -39,22 +39,15 @@ async function lookupApp(applicationId: string): Promise { return socket.application; } -let hideSetting = false; let ws: WebSocket; -if (IS_VESKTOP || IS_EQUIBOP || "legcord" in window) { - hideSetting = true; -} else if ("goofcord" in window) { - hideSetting = false; -} - migratePluginSettings("WebRichPresence", "WebRichPresence (arRPC)"); export default definePlugin({ name: "WebRichPresence", description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)", authors: [Devs.Ducko], reporterTestable: ReporterTestable.None, - hidden: hideSetting, + hidden: !IS_EQUIBOP && !IS_VESKTOP && !("legcord" in window), settingsAboutComponent: () => ( <> diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 377282e33..fa05b3d19 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -119,18 +119,24 @@ export function tryOrElse(func: () => T, fallback: T): T { } } -export function isEquicordGuild(id: string, isGuildId: boolean = false): boolean { +export function isEquicordGuild(id: string | null | undefined, isGuildId: boolean = false): boolean { + if (!id) return false; + if (isGuildId) return id === GUILD_ID; const channel = ChannelStore.getChannel(id); return channel.guild_id === GUILD_ID; } -export function isSupportChannel(channelId: string): boolean { +export function isSupportChannel(channelId: string | null | undefined): boolean { + if (channelId) return false; + return channelId === SUPPORT_CHANNEL_ID; } -export function isEquicordSupport(userId: string): boolean { +export function isEquicordSupport(userId: string | null | undefined): boolean { + if (!userId) return false; + const member = GuildMemberStore.getMember(GUILD_ID, userId); return member?.roles?.includes(EQUICORD_HELPERS) || false; }