2025-01-23 03:59:41 -05:00
/ *
* Vencord , a Discord client mod
* Copyright ( c ) 2024 Vendicated and contributors
* SPDX - License - Identifier : GPL - 3.0 - or - later
* /
2024-11-01 13:10:56 -04:00
import { exec , spawn } from "child_process" ;
2025-01-23 03:59:41 -05:00
import { BrowserView , BrowserWindow , dialog , shell } from "electron" ;
import { existsSync , readdirSync , readFileSync } from "fs" ;
import { rm } from "fs/promises" ;
import { join } from "path" ;
2024-11-01 15:36:58 -04:00
const PLUGIN_META_REGEX = /export default definePlugin\((?:\s|\/(?:\/|\*).*)*{\s*(?:\s|\/(?:\/|\*).*)*name:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*,(?:\s|\/(?:\/|\*).*)*(?:\s|\/(?:\/|\*).*)*description:\s*(?:"|'|`)(.*)(?:"|'|`)(?:\s|\/(?:\/|\*).*)*/ ;
2025-01-23 03:59:41 -05:00
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 ) {
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 ;
}
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 ( ) ;
}
2024-11-01 13:10:56 -04:00
2025-01-23 03:59:41 -05:00
async function build ( ) : Promise < any > {
2024-11-01 13:10:56 -04:00
return new Promise ( ( resolve , reject ) = > {
2025-01-23 03:59:41 -05:00
const proc = exec ( "pnpm build" , {
cwd : __dirname
} ) ;
proc . once ( "close" , async ( ) = > {
if ( proc . exitCode !== 0 ) {
reject ( "Failed to build" ) ;
2024-11-01 13:10:56 -04:00
}
2025-01-23 03:59:41 -05:00
resolve ( "Success" ) ;
2024-11-01 13:10:56 -04:00
} ) ;
} ) ;
}
2024-11-01 15:36:58 -04:00
2025-01-23 03:59:41 -05:00
async function getPluginMeta ( path : string ) : Promise < {
name : string ;
description : string ;
usesPreSend : boolean ;
usesNative : boolean ;
} > {
2024-11-01 15:36:58 -04:00
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" ;
} ) ;
2025-01-23 03:59:41 -05:00
if ( ! fileToRead ) reject ( "Invalid plugin" ) ;
2024-11-01 15:36:58 -04:00
const file = readFileSync ( ` ${ path } / ${ fileToRead } ` , "utf8" ) ;
const rawMeta = file . match ( PLUGIN_META_REGEX ) ;
resolve ( {
name : rawMeta ! [ 1 ] ,
description : rawMeta ! [ 2 ] ,
2025-01-23 03:59:41 -05:00
usesPreSend : file.includes ( "PreSendListener" ) ,
usesNative : files.includes ( "native.ts" ) || files . includes ( "native.js" )
2024-11-01 15:36:58 -04:00
} ) ;
} ) ;
}
2025-01-23 03:59:41 -05:00
async function cloneRepo ( link : string , repo : string ) : Promise < void > {
2024-11-01 15:36:58 -04:00
return new Promise ( ( resolve , reject ) = > {
2025-01-23 03:59:41 -05:00
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 ) ;
2024-11-01 15:36:58 -04:00
}
2025-01-23 03:59:41 -05:00
resolve ( ) ;
2024-11-01 15:36:58 -04:00
} ) ;
} ) ;
}
2025-01-23 03:59:41 -05:00
function generateReviewPluginContent ( meta : {
name : string ;
description : string ;
usesPreSend : boolean ;
usesNative : boolean ;
} ) : string {
const template = `
< ! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > Review userplugin < / title >
< script >
document . addEventListener ( "DOMContentLoaded" , ( ) = > {
document . querySelector ( "#abort" ) . addEventListener ( "click" , ( ) = > {
document . title = "abortInstall" ;
} )
document . querySelector ( "#review" ) . addEventListener ( "click" , ( ) = > {
document . title = "reviewCode" ;
} )
document . querySelector ( "#install" ) . addEventListener ( "click" , ( ) = > {
if ( ! document . querySelector ( "style" ) . innerHTML . includes ( "#native-ts-warning { display: none !important; }" ) && ! document . querySelector ( "input[type='checkbox']" ) . checked ) return alert ( "Make sure to acknowledge all warnings before installing." ) ;
document . title = "install" ;
} )
} )
< / script >
< style >
* {
font - family : sans - serif ;
color : white ;
}
body {
background - color : # 202020 ;
padding : 10px ;
display : flex ;
flex - direction : column ;
}
. card {
padding : 10px 12 px ;
border : 1px solid grey ;
border - radius : 10px ;
margin - bottom : 10px ; /* Added margin for better spacing */
}
. warn - card {
border - color : # ffde00 ;
background - color : # ffde0011 ;
}
. danger - card {
border - color : # ff0000 ;
background - color : # ff000022 ;
h3 {
color : # ffaaaa ;
}
}
. card * {
margin : 0 ;
}
. card h3 {
margin - bottom : 5px ;
}
input [ type = "checkbox" ] {
margin - right : 5px ;
}
# validate {
color : # ffaaaa ;
}
# btn - row {
display : flex ;
flex : 1 ;
}
# btn - row * {
flex :1 ;
margin : 5px ;
padding : 10px 5 px ;
border : 1px solid white ;
border - radius : 5px ;
}
# abort {
background - color : # aa3030 ;
}
# review {
background - color : # 303030 ;
}
# install {
background - color : # 002000 ;
}
# abort :hover {
background - color : # bb3030 ;
font - weight : bold ;
}
# review :hover {
background - color : # 404040 ;
font - weight : bold ;
}
# install :hover {
background - color : # 003000 ;
font - weight : bold ;
}
% WARNINGHIDER %
% NATIVETSHIDER %
% PRESENDHIDER %
< / style >
< / head >
< body >
< h2 > Plugin info < / h2 >
< div class = "card" id = "plugin-info-card" >
< h3 > % PLUGINNAME % < / h3 >
< p > % PLUGINDESC % < / p >
< / div >
< h2 data-useless = "warning" > Warnings < / h2 >
< div data-useless = "warning" class = "card danger-card" id = "native-ts-warning" >
< h3 > Uses a native . ts file < / h3 >
< p >
Use of this file allows the plugin to escape the browser sandbox and
potentially do anything to your system and data .
< b
> ONLY INSTALL THIS PLUGIN AFTER REVIEWING THE CODE AND BEING 100 % SURE
THAT NOTHING BAD CAN BE DONE ! < / b
>
< / p >
< br / >
< input type = "checkbox" / > Acknowledge warning ( required to allow install )
< / div >
< div data-useless = "warning" class = "card warn-card" id = "pre-send-warning" >
< h3 > Has pre - send listeners < / h3 >
< p > This allows the plugin to edit your messages before they are sent . < / p >
< / div >
< p id = "validate" >
Reminder : installing a userplugin can be a destructive action . Make sure that you know and trust the developer before installing any .
< / p >
< div id = "btn-row" >
< button id = "abort" >
Cancel installation
< / button >
< button id = "review" >
Review source code
< / button >
< button id = "install" >
Install plugin
< / button >
< / div >
< / body >
< / html >
` .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 } ` ;
2024-11-01 15:36:58 -04:00
}