From c9be6181644a329ce6556c49186e56e2b7d5e8e2 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sun, 2 Feb 2025 03:17:37 +0100 Subject: [PATCH] add main to renderer command API --- src/main/arrpc.ts | 14 ++++----- src/main/index.ts | 2 ++ src/main/ipcCommands.ts | 56 ++++++++++++++++++++++++++++++++++++ src/main/mainWindow.ts | 9 +++--- src/preload/VesktopNative.ts | 12 ++++---- src/renderer/arrpc.ts | 40 ++++++++++++++++++++++++++ src/renderer/index.ts | 32 ++------------------- src/renderer/ipcCommands.ts | 49 +++++++++++++++++++++++++++++++ src/shared/IpcEvents.ts | 11 ++++++- 9 files changed, 176 insertions(+), 49 deletions(-) create mode 100644 src/main/ipcCommands.ts create mode 100644 src/renderer/arrpc.ts create mode 100644 src/renderer/ipcCommands.ts diff --git a/src/main/arrpc.ts b/src/main/arrpc.ts index 1899d9c..b3b5c95 100644 --- a/src/main/arrpc.ts +++ b/src/main/arrpc.ts @@ -5,9 +5,9 @@ */ import Server from "arrpc"; -import { IpcEvents } from "shared/IpcEvents"; +import { IpcCommands } from "shared/IpcEvents"; -import { mainWin } from "./mainWindow"; +import { sendRendererCommand } from "./ipcCommands"; import { Settings } from "./settings"; let server: any; @@ -19,16 +19,12 @@ export async function initArRPC() { try { server = await new Server(); - server.on("activity", (data: any) => mainWin.webContents.send(IpcEvents.ARRPC_ACTIVITY, JSON.stringify(data))); - server.on("invite", (invite: string, callback: (valid: boolean) => void) => { + server.on("activity", (data: any) => sendRendererCommand(IpcCommands.RPC_ACTIVITY, JSON.stringify(data))); + server.on("invite", async (invite: string, callback: (valid: boolean) => void) => { invite = String(invite); if (!inviteCodeRegex.test(invite)) return callback(false); - mainWin.webContents - // Safety: Result of JSON.stringify should always be safe to equal - // Also, just to be super super safe, invite is regex validated above - .executeJavaScript(`Vesktop.openInviteModal(${JSON.stringify(invite)})`) - .then(callback); + await sendRendererCommand(IpcCommands.RPC_INVITE, invite).then(callback); }); } catch (e) { console.error("Failed to start arRPC server", e); diff --git a/src/main/index.ts b/src/main/index.ts index e18d45d..6f36a5d 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -23,6 +23,8 @@ if (IS_DEV) { autoUpdater.checkForUpdatesAndNotify(); } +console.log("Vesktop v" + app.getVersion()); + // Make the Vencord files use our DATA_DIR process.env.VENCORD_USER_DATA_DIR = DATA_DIR; diff --git a/src/main/ipcCommands.ts b/src/main/ipcCommands.ts new file mode 100644 index 0000000..2349b75 --- /dev/null +++ b/src/main/ipcCommands.ts @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { randomUUID } from "crypto"; +import { ipcMain } from "electron"; +import { IpcEvents } from "shared/IpcEvents"; + +import { mainWin } from "./mainWindow"; + +const resolvers = new Map void>>(); + +export interface IpcMessage { + nonce: string; + message: string; + data?: any; +} + +export interface IpcResponse { + nonce: string; + ok: boolean; + data?: any; +} + +/** + * Sends a message to the renderer process and waits for a response. + * `data` must be serializable as it will be sent over IPC. + * + * You must add a handler for the message in the renderer process. + */ +export function sendRendererCommand(message: string, data?: any) { + const nonce = randomUUID(); + + const promise = new Promise((resolve, reject) => { + resolvers.set(nonce, { resolve, reject }); + }); + + mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data }); + + return promise; +} + +ipcMain.on(IpcEvents.IPC_COMMAND, (_event, { nonce, ok, data }: IpcResponse) => { + const resolver = resolvers.get(nonce); + if (!resolver) throw new Error(`Unknown message: ${nonce}`); + + if (ok) { + resolver.resolve(data); + } else { + resolver.reject(data); + } + + resolvers.delete(nonce); +}); diff --git a/src/main/mainWindow.ts b/src/main/mainWindow.ts index d860b37..d7c124d 100644 --- a/src/main/mainWindow.ts +++ b/src/main/mainWindow.ts @@ -18,7 +18,7 @@ import { } from "electron"; import { rm } from "fs/promises"; import { join } from "path"; -import { IpcEvents } from "shared/IpcEvents"; +import { IpcCommands, IpcEvents } from "shared/IpcEvents"; import { isTruthy } from "shared/utils/guards"; import { once } from "shared/utils/once"; import type { SettingsStore } from "shared/utils/SettingsStore"; @@ -36,6 +36,7 @@ import { MIN_WIDTH, VENCORD_FILES_DIR } from "./constants"; +import { sendRendererCommand } from "./ipcCommands"; import { Settings, State, VencordSettings } from "./settings"; import { createSplashWindow } from "./splash"; import { makeLinksOpenExternally } from "./utils/makeLinksOpenExternally"; @@ -198,9 +199,7 @@ function initMenuBar(win: BrowserWindow) { label: "Settings", accelerator: "CmdOrCtrl+,", async click() { - mainWin.webContents.executeJavaScript( - "Vencord.Webpack.Common.SettingsRouter.open('My Account')" - ); + sendRendererCommand(IpcCommands.NAVIGATE_SETTINGS); } }, { @@ -366,7 +365,7 @@ function initSettingsListeners(win: BrowserWindow) { } async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) { - languages ??= await win.webContents.executeJavaScript("[...new Set(navigator.languages)]").catch(() => []); + languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES); if (!languages) return; const ses = session.defaultSession; diff --git a/src/preload/VesktopNative.ts b/src/preload/VesktopNative.ts index 136bcf1..1d200bc 100644 --- a/src/preload/VesktopNative.ts +++ b/src/preload/VesktopNative.ts @@ -6,6 +6,7 @@ import { Node } from "@vencord/venmic"; import { ipcRenderer } from "electron"; +import { IpcMessage, IpcResponse } from "main/ipcCommands"; import type { Settings } from "shared/settings"; import { IpcEvents } from "../shared/IpcEvents"; @@ -70,11 +71,6 @@ export const VesktopNative = { startSystem: (exclude: Node[]) => invoke(IpcEvents.VIRT_MIC_START_SYSTEM, exclude), stop: () => invoke(IpcEvents.VIRT_MIC_STOP) }, - arrpc: { - onActivity(cb: (data: string) => void) { - ipcRenderer.on(IpcEvents.ARRPC_ACTIVITY, (_, data: string) => cb(data)); - } - }, clipboard: { copyImage: (imageBuffer: Uint8Array, imageSrc: string) => invoke(IpcEvents.CLIPBOARD_COPY_IMAGE, imageBuffer, imageSrc) @@ -82,5 +78,11 @@ export const VesktopNative = { debug: { launchGpu: () => invoke(IpcEvents.DEBUG_LAUNCH_GPU), launchWebrtcInternals: () => invoke(IpcEvents.DEBUG_LAUNCH_WEBRTC_INTERNALS) + }, + commands: { + onCommand(cb: (message: IpcMessage) => void) { + ipcRenderer.on(IpcEvents.IPC_COMMAND, (_, message) => cb(message)); + }, + respond: (response: IpcResponse) => ipcRenderer.send(IpcEvents.IPC_COMMAND, response) } }; diff --git a/src/renderer/arrpc.ts b/src/renderer/arrpc.ts new file mode 100644 index 0000000..ecf5319 --- /dev/null +++ b/src/renderer/arrpc.ts @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { onceReady } from "@vencord/types/webpack"; +import { FluxDispatcher, InviteActions } from "@vencord/types/webpack/common"; +import { IpcCommands } from "shared/IpcEvents"; + +import { onIpcCommand } from "./ipcCommands"; +import { Settings } from "./settings"; + +const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as { + handleEvent(e: MessageEvent): void; +}; + +onIpcCommand(IpcCommands.RPC_ACTIVITY, async data => { + if (!Settings.store.arRPC) return; + + await onceReady; + + arRPC.handleEvent(new MessageEvent("message", { data })); +}); + +onIpcCommand(IpcCommands.RPC_INVITE, async code => { + const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal"); + if (!invite) return false; + + VesktopNative.win.focus(); + + FluxDispatcher.dispatch({ + type: "INVITE_MODAL_OPEN", + invite, + code, + context: "APP" + }); + + return true; +}); diff --git a/src/renderer/index.ts b/src/renderer/index.ts index e8ad31c..656acd2 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -8,12 +8,14 @@ import "./fixes"; import "./appBadge"; import "./patches"; import "./themedSplash"; +import "./ipcCommands"; +import "./arrpc"; console.log("read if cute :3"); export * as Components from "./components"; import { findByPropsLazy, onceReady } from "@vencord/types/webpack"; -import { Alerts, FluxDispatcher } from "@vencord/types/webpack/common"; +import { Alerts } from "@vencord/types/webpack/common"; import SettingsUi from "./components/settings/Settings"; import { Settings } from "./settings"; @@ -21,22 +23,6 @@ export { Settings }; const InviteActions = findByPropsLazy("resolveInvite"); -export async function openInviteModal(code: string) { - const { invite } = await InviteActions.resolveInvite(code, "Desktop Modal"); - if (!invite) return false; - - VesktopNative.win.focus(); - - FluxDispatcher.dispatch({ - type: "INVITE_MODAL_OPEN", - invite, - code, - context: "APP" - }); - - return true; -} - const customSettingsSections = ( Vencord.Plugins.plugins.Settings as any as { customSections: ((ID: Record) => any)[] } ).customSections; @@ -48,18 +34,6 @@ customSettingsSections.push(() => ({ className: "vc-vesktop-settings" })); -const arRPC = Vencord.Plugins.plugins["WebRichPresence (arRPC)"] as any as { - handleEvent(e: MessageEvent): void; -}; - -VesktopNative.arrpc.onActivity(async data => { - if (!Settings.store.arRPC) return; - - await onceReady; - - arRPC.handleEvent(new MessageEvent("message", { data })); -}); - // TODO: remove soon const vencordDir = "vencordDir" as keyof typeof Settings.store; if (Settings.store[vencordDir]) { diff --git a/src/renderer/ipcCommands.ts b/src/renderer/ipcCommands.ts new file mode 100644 index 0000000..cf96435 --- /dev/null +++ b/src/renderer/ipcCommands.ts @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * Vesktop, a desktop app aiming to give you a snappier Discord Experience + * Copyright (c) 2023 Vendicated and Vencord contributors + */ + +import { SettingsRouter } from "@vencord/types/webpack/common"; +import { IpcCommands } from "shared/IpcEvents"; + +type IpcCommandHandler = (data: any) => any; + +const handlers = new Map(); + +function respond(nonce: string, ok: boolean, data: any) { + VesktopNative.commands.respond({ nonce, ok, data }); +} + +VesktopNative.commands.onCommand(async ({ message, nonce, data }) => { + const handler = handlers.get(message); + if (!handler) { + return respond(nonce, false, `No handler for message: ${message}`); + } + + try { + const result = await handler(data); + respond(nonce, true, result); + } catch (err) { + respond(nonce, false, String(err)); + } +}); + +export function onIpcCommand(channel: string, handler: IpcCommandHandler) { + if (handlers.has(channel)) { + throw new Error(`Handler for message ${channel} already exists`); + } + + handlers.set(channel, handler); +} + +export function offIpcCommand(channel: string) { + handlers.delete(channel); +} + +/* Generic Handlers */ + +onIpcCommand(IpcCommands.NAVIGATE_SETTINGS, () => { + SettingsRouter.open("My Account"); +}); +onIpcCommand(IpcCommands.GET_LANGUAGES, () => navigator.languages); diff --git a/src/shared/IpcEvents.ts b/src/shared/IpcEvents.ts index 0497d14..2de1c34 100644 --- a/src/shared/IpcEvents.ts +++ b/src/shared/IpcEvents.ts @@ -53,5 +53,14 @@ export const enum IpcEvents { CLIPBOARD_COPY_IMAGE = "VCD_CLIPBOARD_COPY_IMAGE", DEBUG_LAUNCH_GPU = "VCD_DEBUG_LAUNCH_GPU", - DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC" + DEBUG_LAUNCH_WEBRTC_INTERNALS = "VCD_DEBUG_LAUNCH_WEBRTC", + + IPC_COMMAND = "VCD_IPC_COMMAND" +} + +export const enum IpcCommands { + RPC_ACTIVITY = "rpc:activity", + RPC_INVITE = "rpc:invite", + NAVIGATE_SETTINGS = "navigate:settings", + GET_LANGUAGES = "navigator.languages" }