175 lines
7.2 KiB
TypeScript
175 lines
7.2 KiB
TypeScript
/*
|
|
* 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<string> {
|
|
// 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<any> {
|
|
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<void> {
|
|
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}`;
|
|
}
|