diff --git a/UserpluginInstallButton.tsx b/UserpluginInstallButton.tsx index 4246824..71032a9 100644 --- a/UserpluginInstallButton.tsx +++ b/UserpluginInstallButton.tsx @@ -1,11 +1,17 @@ -import { Alerts, Button, ChannelStore, Toasts } from "@webpack/common"; -import { Message } from "discord-types/general"; -import { CLONE_LINK_REGEX, clonePlugin, Native, plugins } from "."; +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ -const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900"]; +import { Alerts, Button, ChannelStore } from "@webpack/common"; + +import { CLONE_LINK_REGEX, Native, plugins } from "."; + +const WHITELISTED_SHARE_CHANNELS = ["1256395889354997771", "1032200195582197831", "1301947896601509900", "1322935137591365683"]; export default function UserpluginInstallButton({ props }: any) { - const message: Message = props.message; + const { message } = props; if (!WHITELISTED_SHARE_CHANNELS.includes(ChannelStore.getChannel(message.channel_id).parent_id) && !WHITELISTED_SHARE_CHANNELS.includes(message.channel_id)) return; const gitLink = (props.message.content as string).match(CLONE_LINK_REGEX); @@ -16,7 +22,19 @@ export default function UserpluginInstallButton({ props }: any) { { @@ -26,33 +44,7 @@ export default function UserpluginInstallButton({ props }: any) { }} color={Button.Colors.RED} onClick={() => { - Alerts.show({ - title: "Uninstall plugin", - body: `Are you sure that you want to uninstall ${gitLink[1]}?`, - cancelText: "Cancel", - confirmColor: Button.Colors.RED, - confirmText: "Uninstall", - async onConfirm() { - Toasts.show({ - id: Toasts.genId(), - message: `Uninstalling ${gitLink[1]}...`, - type: Toasts.Type.MESSAGE - }); - try { - await Native.deleteFolder(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`); - await Native.build(VesktopNative.fileManager.getVencordDir().replace("\\", "/")); - window.location.reload(); - } - catch { - Toasts.pop(); - return Toasts.show({ - message: "Something bad has happened while deleting the plugin.", - id: Toasts.genId(), - type: Toasts.Type.FAILURE - }); - } - }, - }); + }}> Uninstall plugin diff --git a/index.tsx b/index.tsx index c90107c..07611de 100644 --- a/index.tsx +++ b/index.tsx @@ -1,57 +1,30 @@ -import { Notices } from "@api/index"; -import { addAccessory, removeAccessory } from "@api/MessageAccessories"; -import { Devs } from "@utils/constants"; -import definePlugin, { PluginNative } from "@utils/types"; -import { Alerts, Button, ChannelStore, Forms, TextInput, Toasts, Text } from "@webpack/common"; -import { Message } from "discord-types/general"; -import { clone } from "lodash"; -import UserpluginInstallButton from "./UserpluginInstallButton"; -import { showInstallModal } from "./UserpluginInstallModal"; +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ import "./style.css"; -export let plugins: any[] = []; -export 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)?(?:\/)?/; +import { Devs } from "@utils/constants"; +import definePlugin, { PluginNative } from "@utils/types"; +import { Alerts, TextInput } from "@webpack/common"; + +import UserpluginInstallButton from "./UserpluginInstallButton"; + +export const plugins: any[] = []; +export 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)?(?:\/)?/; + // @ts-ignore export const Native = VencordNative.pluginHelpers.UserpluginInstaller as PluginNative; -export async function clonePlugin(gitLink: RegExpMatchArray) { - Toasts.show({ - message: "Cloning plugin...", - id: Toasts.genId(), - type: Toasts.Type.MESSAGE - }); - try { - const path = `${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${gitLink[1]}`; - await Native.cloneRepo(gitLink[0], path); - const meta = await Native.getPluginMeta(path); - console.log(meta); - showInstallModal(meta, path); - } - catch (e) { - Toasts.pop(); - return Toasts.show({ - message: "Something bad has happened while cloning the plugin, try again later and make sure that the plugin link is valid.", - id: Toasts.genId(), - type: Toasts.Type.FAILURE - }); - } -} - export default definePlugin({ name: "UserpluginInstaller", description: "Install userplugins with a simple button click", authors: [Devs.nin0dev], - async start() { - plugins = await Native.getPlugins(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/`); - console.log(plugins); - addAccessory("userpluginInstallButton", (props: Record) => ( - - ), 4); - }, - stop() { - removeAccessory("userpluginInstallButton"); + renderMessageAccessory: (props) => { + return ; }, toolboxActions: { "Install Plugin": () => { @@ -62,50 +35,15 @@ export default definePlugin({ { gitUrl = v; }} placeholder="Git link (https://github.com/...)" /> , confirmText: "Install", - onConfirm() { - const fullGitLink = gitUrl.match(CLONE_LINK_REGEX); - if (!fullGitLink) return; - clonePlugin(fullGitLink); + async onConfirm() { + const gitLink = gitUrl.match(CLONE_LINK_REGEX)!; + await Native.initPluginInstall(gitLink[0], gitLink[1], gitLink[2], gitLink[3]); + window.location.reload(); } }); }, "Uninstall Plugin": () => { - let name = ""; - Alerts.show({ - title: "Uninstall plugin", - body: <> - Out of these plugins, which would you like to uninstall? - { - plugins.map((item, i) => - {item} - ) - } - { name = v; }} style={{ marginTop: "10px" }} placeholder="Plugin name as written above" /> - , - confirmText: "Uninstall", - confirmColor: Button.Colors.RED, - async onConfirm() { - if (!plugins.includes(name)) return; - Toasts.show({ - id: Toasts.genId(), - message: `Uninstalling ${name}...`, - type: Toasts.Type.MESSAGE - }); - try { - await Native.deleteFolder(`${VesktopNative.fileManager.getVencordDir().replace("\\", "/")}/../src/userplugins/${name}`); - await Native.build(VesktopNative.fileManager.getVencordDir().replace("\\", "/")); - window.location.reload(); - } - catch { - Toasts.pop(); - return Toasts.show({ - message: "Something bad has happened while deleting the plugin.", - id: Toasts.genId(), - type: Toasts.Type.FAILURE - }); - } - } - }); + } } }); diff --git a/native.ts b/native.ts index 210f5cf..d417258 100644 --- a/native.ts +++ b/native.ts @@ -1,24 +1,119 @@ +/* + * 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 { IpcMainInvokeEvent } from "electron"; -import { read, readdir, readdirSync, readFileSync, rmSync } from "fs"; +import { BrowserView, BrowserWindow, dialog, shell } from "electron"; +import { existsSync, readdirSync, readFileSync } from "fs"; +import { rm } from "fs/promises"; +import { join } from "path"; 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 cloneRepo(_: IpcMainInvokeEvent, repo: string, clonePath: string): Promise { - rmSync(clonePath, { recursive: true, force: true }); - return new Promise((resolve, reject) => { - exec(`git clone ${repo} ${clonePath}`, (error, stdout, stderr) => { - if (error) { - return reject( - stderr - ); +export async function initPluginInstall(_, link: string, source: string, owner: string, repo: string) { + const verifiedRegex = link.match(CLONE_LINK_REGEX)!; + if (verifiedRegex.length !== 4 || verifiedRegex[0] !== link || verifiedRegex[1] !== source || verifiedRegex[2] !== owner || verifiedRegex[3] !== repo) return; + + // 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: { + throw "Rejected by user"; + } + case 1: { + await cloneRepo(link, repo); + break; + } + case 2: { + await shell.openExternal(link); + throw "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 + }); + throw "Rejected by user"; + break; } - resolve(null); + case "reviewCode": { + win.close(); + shell.openPath(join(__dirname, "..", "src", "userplugins", repo)); + throw "A file explorer window should've been opened with your plugin"; + break; + } + case "install": { + win.close(); + await build(); + 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"); }); }); } -export async function getPluginMeta(_, path: string): Promise { +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; @@ -28,40 +123,192 @@ export async function getPluginMeta(_, path: string): Promise { if (f === "index.js") fileToRead = "index.js"; if (f === "index.jsx") fileToRead = "index.jsx"; }); - if (!fileToRead) reject(); + 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") + usesPreSend: file.includes("PreSendListener"), + usesNative: files.includes("native.ts") || files.includes("native.js") }); }); } -export function deleteFolder(_, path: string) { - if(path.match(/\.\./g).length > 1) return; - rmSync(path, { recursive: true, force: true }); -} - -export async function build(_: IpcMainInvokeEvent, path: string): Promise { +async function cloneRepo(link: string, repo: string): Promise { return new Promise((resolve, reject) => { - exec(`pnpm build`, { - cwd: path - }, (error, stdout, stderr) => { - if (error) { - return reject( - stderr - ); + 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(null); + resolve(); }); }); } -export async function getPlugins(_, path: string): Promise { - return new Promise((resolve) => { - return resolve(readdirSync(path)); - }); +function generateReviewPluginContent(meta: { + name: string; + description: string; + usesPreSend: boolean; + usesNative: boolean; +}): string { + const template = ` + + + + + + Review userplugin + + + + +

Plugin info

+
+

%PLUGINNAME%

+

%PLUGINDESC%

+
+ +

Warnings

+
+

Uses a native.ts file

+

+ Use of this file allows the plugin to escape the browser sandbox and + potentially do anything to your system and data. + ONLY INSTALL THIS PLUGIN AFTER REVIEWING THE CODE AND BEING 100% SURE + THAT NOTHING BAD CAN BE DONE! +

+
+ Acknowledge warning (required to allow install) +
+
+

Has pre-send listeners

+

This allows the plugin to edit your messages before they are sent.

+
+

+ Reminder: installing a userplugin can be a destructive action. Make sure that you know and trust the developer before installing any. +

+
+ + + +
+ + +`.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}`; }