/* * Vencord, a Discord client mod * Copyright (c) 2024 Vendicated and contributors * SPDX-License-Identifier: GPL-3.0-or-later */ import { exec, spawn } from "child_process"; import { BrowserView, BrowserWindow, dialog, shell } from "electron"; import { existsSync, readdirSync, readFileSync } from "fs"; import { rm } from "fs/promises"; import { join } from "path"; // @ts-ignore fuck off import pluginValidateContent from "./pluginValidate.txt"; // i would use HTML but esbuild is being whiny const PLUGIN_META_REGEX = /export default definePlugin\((?:\s|\/(?:\/|\*).*)*{\s*(?:\s|\/(?:\/|\*).*)*name:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*,(?:\s|\/(?:\/|\*).*)*(?:\s|\/(?:\/|\*).*)*description:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*/; const CLONE_LINK_REGEX = /https:\/\/((?:git(?:hub|lab)\.com|git\.(?:[a-zA-Z0-9]|\.)+|codeberg\.org))\/(?!user-attachments)((?:[a-zA-Z0-9]|-)+)\/((?:[a-zA-Z0-9]|-)+)(?:\.git)?(?:\/)?/; export async function initPluginInstall(_, link: string, source: string, owner: string, repo: string): Promise { // eslint-disable-next-line return new Promise(async (resolve, reject) => { const verifiedRegex = link.match(CLONE_LINK_REGEX)!; if (verifiedRegex.length !== 4 || verifiedRegex[0] !== link || verifiedRegex[1] !== source || verifiedRegex[2] !== owner || verifiedRegex[3] !== repo) return reject("Invalid link"); // Ask for clone const cloneDialog = await dialog.showMessageBox({ title: "Clone userplugin", message: `You are about to clone a userplugin from ${source}.`, type: "question", detail: `The repository name is "${repo}" and it is owned by "${owner}".\nThe repository URL is ${link}\n\n(If you did not request this intentionally, choose Cancel)`, buttons: ["Cancel", "Clone repository and continue install", "Open repository in browser"] }); switch (cloneDialog.response) { case 0: { return reject("Rejected by user"); } case 1: { await cloneRepo(link, repo); break; } case 2: { await shell.openExternal(link); return reject("silentStop"); } } // Get plugin meta const meta = await getPluginMeta(join(__dirname, "..", "src", "userplugins", repo)); // Review plugin const win = new BrowserWindow({ maximizable: false, minimizable: false, width: 560, height: meta.usesNative || meta.usesPreSend ? 650 : 360, resizable: false, webPreferences: { devTools: true }, title: "Review userplugin", modal: true, parent: BrowserWindow.getAllWindows()[0], show: false, autoHideMenuBar: true }); const reView /* haha got it */ = new BrowserView({ webPreferences: { devTools: true, nodeIntegration: true } }); win.setBrowserView(reView); win.addBrowserView(reView); win.setTopBrowserView(reView); win.loadURL(generateReviewPluginContent(meta)); win.on("page-title-updated", async e => { switch (win.webContents.getTitle() as "abortInstall" | "reviewCode" | "install") { case "abortInstall": { win.close(); await rm(join(__dirname, "..", "src", "userplugins", repo), { recursive: true }); return reject("Rejected by user"); } case "install": { win.close(); await build(); resolve(meta.name); break; } } }); win.show(); }); } async function build(): Promise { return new Promise((resolve, reject) => { const proc = exec("pnpm build", { cwd: __dirname }); proc.once("close", async () => { if (proc.exitCode !== 0) { reject("Failed to build"); } resolve("Success"); }); }); } async function getPluginMeta(path: string): Promise<{ name: string; description: string; usesPreSend: boolean; usesNative: boolean; }> { return new Promise((resolve, reject) => { const files = readdirSync(path); let fileToRead: "index.ts" | "index.tsx" | "index.js" | "index.jsx" | undefined; files.forEach(f => { if (f === "index.ts") fileToRead = "index.ts"; if (f === "index.tsx") fileToRead = "index.tsx"; if (f === "index.js") fileToRead = "index.js"; if (f === "index.jsx") fileToRead = "index.jsx"; }); if (!fileToRead) reject("Invalid plugin"); const file = readFileSync(`${path}/${fileToRead}`, "utf8"); const rawMeta = file.match(PLUGIN_META_REGEX); resolve({ name: rawMeta![1], description: rawMeta![2], usesPreSend: file.includes("PreSendListener"), usesNative: files.includes("native.ts") || files.includes("native.js") }); }); } async function cloneRepo(link: string, repo: string): Promise { return new Promise((resolve, reject) => { const proc = spawn("git", ["clone", link], { cwd: join(__dirname, "..", "src", "userplugins") }); proc.once("close", async () => { if (proc.exitCode !== 0) { if (!existsSync(join(__dirname, "..", "src", "userplugins", repo))) return reject("Failed to clone"); const deleteReqDialog = await dialog.showMessageBox({ title: "Error", message: "Plugin already exists", type: "error", detail: `The plugin that you tried to clone already exists at ${join(__dirname, "..", "src", "userplugins")}.\nWould you like to reclone it? Only do this if you want to reinstall or update the plugin.`, buttons: ["No", "Yes"] }); if (deleteReqDialog.response !== 1) return reject("User rejected"); await rm(join(__dirname, "..", "src", "userplugins", repo), { recursive: true }); await cloneRepo(link, repo); } resolve(); }); }); } function generateReviewPluginContent(meta: { name: string; description: string; usesPreSend: boolean; usesNative: boolean; }): string { const template = pluginValidateContent.replace("%PLUGINNAME%", meta.name.replaceAll("<", "<")).replace("%PLUGINDESC%", meta.description.replaceAll("<", "<")).replace("%WARNINGHIDER%", !meta.usesNative && !meta.usesPreSend ? "[data-useless=\"warning\"] { display: none !important; }" : "").replace("%NATIVETSHIDER%", meta.usesNative ? "" : "#native-ts-warning { display: none !important; }").replace("%PRESENDHIDER%", meta.usesPreSend ? "" : "#pre-send-warning { display: none !important; }"); const buf = Buffer.from(template).toString("base64"); return `data:text/html;base64,${buf}`; }