2023-05-20 00:37:21 +02:00
import path from "path" ;
import vm from "vm" ;
2023-05-19 22:38:45 +02:00
import Logger from "@common/logger" ;
2023-05-20 00:37:21 +02:00
import Config from "@data/config" ;
import AddonError from "@structs/addonerror" ;
2019-06-27 22:18:40 +02:00
import AddonManager from "./addonmanager" ;
2019-06-08 08:35:43 +02:00
import Settings from "./settingsmanager" ;
2019-06-25 22:36:34 +02:00
import Strings from "./strings" ;
2021-03-06 09:30:16 +01:00
import Events from "./emitter" ;
2019-06-23 06:11:50 +02:00
2023-05-20 00:37:21 +02:00
import Toasts from "@ui/toasts" ;
import Modals from "@ui/modals" ;
import SettingsRenderer from "@ui/settings" ;
2019-06-08 08:35:43 +02:00
2022-07-08 16:42:04 +02:00
const normalizeExports = name => `
2022-06-25 07:57:35 +02:00
if ( module . exports . default ) {
module . exports = module . exports . default ;
}
if ( typeof ( module . exports ) !== "function" ) {
2022-07-08 16:42:04 +02:00
module . exports = eval ( "${name}" ) ;
2022-06-25 07:57:35 +02:00
} ` ;
2019-06-27 22:18:40 +02:00
export default new class PluginManager extends AddonManager {
2019-06-08 08:35:43 +02:00
get name ( ) { return "PluginManager" ; }
get extension ( ) { return ".plugin.js" ; }
2021-04-05 07:43:30 +02:00
get duplicatePattern ( ) { return /\.plugin\s?\([0-9]+\)\.js/ ; }
2019-06-27 22:18:40 +02:00
get addonFolder ( ) { return path . resolve ( Config . dataPath , "plugins" ) ; }
2019-06-08 08:35:43 +02:00
get prefix ( ) { return "plugin" ; }
2019-06-30 07:32:14 +02:00
get language ( ) { return "javascript" ; }
2019-06-08 08:35:43 +02:00
constructor ( ) {
super ( ) ;
this . onSwitch = this . onSwitch . bind ( this ) ;
this . observer = new MutationObserver ( ( mutations ) => {
for ( let i = 0 , mlen = mutations . length ; i < mlen ; i ++ ) {
this . onMutation ( mutations [ i ] ) ;
}
} ) ;
}
2019-05-28 20:19:48 +02:00
2021-03-18 22:50:47 +01:00
initialize ( ) {
const errors = super . initialize ( ) ;
2019-06-24 21:47:24 +02:00
this . setupFunctions ( ) ;
2022-10-03 10:40:18 +02:00
Settings . registerPanel ( "plugins" , Strings . Panels . plugins , {
order : 3 ,
2023-03-20 04:41:39 +01:00
element : SettingsRenderer . getAddonPanel ( Strings . Panels . plugins , this . addonList , this . state , {
2022-10-03 10:40:18 +02:00
type : this . prefix ,
folder : this . addonFolder ,
onChange : this . togglePlugin . bind ( this ) ,
reload : this . reloadPlugin . bind ( this ) ,
refreshList : this . updatePluginList . bind ( this ) ,
saveAddon : this . saveAddon . bind ( this ) ,
editAddon : this . editAddon . bind ( this ) ,
deleteAddon : this . deleteAddon . bind ( this ) ,
2024-02-22 06:06:30 +01:00
enableAll : this . enableAllAddons . bind ( this ) ,
disableAll : this . disableAllAddons . bind ( this ) ,
2022-10-03 10:40:18 +02:00
prefix : this . prefix
} )
} ) ;
2019-06-24 21:47:24 +02:00
return errors ;
}
2019-06-08 08:35:43 +02:00
/* Aliases */
updatePluginList ( ) { return this . updateList ( ) ; }
2019-06-27 22:18:40 +02:00
loadAllPlugins ( ) { return this . loadAllAddons ( ) ; }
2019-05-28 20:19:48 +02:00
2019-06-27 22:18:40 +02:00
enablePlugin ( idOrAddon ) { return this . enableAddon ( idOrAddon ) ; }
disablePlugin ( idOrAddon ) { return this . disableAddon ( idOrAddon ) ; }
togglePlugin ( id ) { return this . toggleAddon ( id ) ; }
2019-05-28 20:19:48 +02:00
2019-06-27 22:18:40 +02:00
unloadPlugin ( idOrFileOrAddon ) { return this . unloadAddon ( idOrFileOrAddon ) ; }
2020-10-06 23:44:10 +02:00
loadPlugin ( filename ) { return this . loadAddon ( filename ) ; }
2019-05-28 20:19:48 +02:00
2021-04-06 20:09:43 +02:00
loadAddon ( filename , shouldCTE = true ) {
2021-04-07 03:40:19 +02:00
const error = super . loadAddon ( filename , shouldCTE ) ;
2021-04-06 20:09:43 +02:00
if ( error && shouldCTE ) Modals . showAddonErrors ( { plugins : [ error ] } ) ;
return error ;
2019-06-08 08:35:43 +02:00
}
2019-05-28 20:19:48 +02:00
2021-03-18 22:50:47 +01:00
reloadPlugin ( idOrFileOrAddon ) {
const error = this . reloadAddon ( idOrFileOrAddon ) ;
2019-06-27 22:18:40 +02:00
if ( error ) Modals . showAddonErrors ( { plugins : [ error ] } ) ;
return typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
2019-06-08 08:35:43 +02:00
}
2019-05-28 20:19:48 +02:00
2019-06-08 08:35:43 +02:00
/* Overrides */
2019-06-27 22:18:40 +02:00
initializeAddon ( addon ) {
2022-02-04 22:23:21 +01:00
if ( ! addon . exports || ! addon . name ) return new AddonError ( addon . name || addon . filename , addon . filename , "Plugin had no exports or @name property" , { message : "Plugin had no exports or no @name property. @name property is required for all addons." , stack : "" } , this . prefix ) ;
2019-06-08 08:35:43 +02:00
try {
2022-06-25 07:57:35 +02:00
const isValid = typeof ( addon . exports ) === "function" ;
if ( ! isValid ) return new AddonError ( addon . name || addon . filename , addon . filename , "Plugin not a valid format." , { message : "Plugins should be either a function or a class" , stack : "" } , this . prefix ) ;
2020-07-29 21:06:54 +02:00
const PluginClass = addon . exports ;
2022-06-25 07:57:35 +02:00
const meta = Object . assign ( { } , addon ) ;
delete meta . exports ;
2022-06-27 00:27:12 +02:00
const thePlugin = PluginClass . prototype ? new PluginClass ( meta ) : addon . exports ( meta ) ;
2022-06-25 08:59:23 +02:00
if ( ! thePlugin . start || ! thePlugin . stop ) return new AddonError ( addon . name || addon . filename , addon . filename , "Missing start or stop function." , { message : "Plugins must have both a start and stop function." , stack : "" } , this . prefix ) ;
2022-06-25 07:57:35 +02:00
2020-07-29 21:06:54 +02:00
addon . instance = thePlugin ;
2022-02-04 22:23:21 +01:00
addon . name = thePlugin . getName ? thePlugin . getName ( ) : addon . name ;
2022-06-27 00:27:12 +02:00
addon . author = thePlugin . getAuthor ? thePlugin . getAuthor ( ) : addon . author ;
addon . description = thePlugin . getDescription ? thePlugin . getDescription ( ) : addon . description ;
addon . version = thePlugin . getVersion ? thePlugin . getVersion ( ) : addon . version ;
if ( ! addon . name || ! addon . author || ! addon . description || ! addon . version ) return new AddonError ( addon . name || addon . filename , addon . filename , "Plugin is missing name, author, description, or version" , { message : "Plugin must provide name, author, description, and version." , stack : "" } , this . prefix ) ;
2019-05-28 20:19:48 +02:00
try {
2020-07-29 21:06:54 +02:00
if ( typeof ( addon . instance . load ) == "function" ) addon . instance . load ( ) ;
2019-05-28 20:19:48 +02:00
}
2019-06-08 08:35:43 +02:00
catch ( error ) {
2019-06-27 22:18:40 +02:00
this . state [ addon . id ] = false ;
2022-10-14 05:42:05 +02:00
return new AddonError ( addon . name , addon . filename , Strings . Addons . methodError . format ( { method : "load()" } ) , { message : error . message , stack : error . stack } , this . prefix ) ;
2019-05-28 20:19:48 +02:00
}
}
2022-06-25 07:57:35 +02:00
catch ( error ) {
2022-10-14 05:42:05 +02:00
return new AddonError ( addon . name , addon . filename , Strings . Addons . methodError . format ( { method : "Plugin constructor()" } ) , { message : error . message , stack : error . stack } , this . prefix ) ;
2022-06-25 07:57:35 +02:00
}
2019-05-28 20:19:48 +02:00
}
2022-07-08 16:42:04 +02:00
requireAddon ( filename ) {
const addon = super . requireAddon ( filename ) ;
try {
const module = { filename , exports : { } } ;
// Test if the code is valid gracefully
2022-09-26 09:01:16 +02:00
vm . compileFunction ( addon . fileContent , [ "require" , "module" , "exports" , "__filename" , "__dirname" ] , { filename : path . basename ( filename ) } ) ;
2022-07-08 16:42:04 +02:00
addon . fileContent += normalizeExports ( addon . exports || addon . name ) ;
addon . fileContent += ` \n //# sourceURL=betterdiscord://plugins/ ${ addon . filename } ` ;
const wrappedPlugin = new Function ( [ "require" , "module" , "exports" , "__filename" , "__dirname" ] , addon . fileContent ) ; // eslint-disable-line no-new-func
wrappedPlugin ( window . require , module , module . exports , module . filename , this . addonFolder ) ;
addon . exports = module . exports ;
delete addon . fileContent ;
return addon ;
}
catch ( err ) {
2022-10-14 05:42:05 +02:00
throw new AddonError ( addon . name || addon . filename , module . filename , Strings . Addons . compileError , { message : err . message , stack : err . stack } , this . prefix ) ;
2022-07-08 16:42:04 +02:00
}
2019-05-28 20:19:48 +02:00
}
2019-06-27 22:18:40 +02:00
startAddon ( id ) { return this . startPlugin ( id ) ; }
stopAddon ( id ) { return this . stopPlugin ( id ) ; }
2020-07-19 01:01:49 +02:00
getAddon ( id ) { return this . getPlugin ( id ) ; }
2019-06-08 08:35:43 +02:00
2019-06-27 22:18:40 +02:00
startPlugin ( idOrAddon ) {
const addon = typeof ( idOrAddon ) == "string" ? this . addonList . find ( p => p . id == idOrAddon ) : idOrAddon ;
if ( ! addon ) return ;
2020-07-29 21:06:54 +02:00
const plugin = addon . instance ;
2019-06-08 08:35:43 +02:00
try {
plugin . start ( ) ;
}
catch ( err ) {
2019-06-27 22:18:40 +02:00
this . state [ addon . id ] = false ;
2024-02-22 06:06:30 +01:00
this . emit ( "disabled" , addon ) ;
2020-07-19 01:01:49 +02:00
Toasts . error ( Strings . Addons . couldNotStart . format ( { name : addon . name , version : addon . version } ) ) ;
2021-03-18 22:50:47 +01:00
Logger . stacktrace ( this . name , ` ${ addon . name } v ${ addon . version } could not be started. ` , err ) ;
2021-04-06 20:09:43 +02:00
return new AddonError ( addon . name , addon . filename , Strings . Addons . enabled . format ( { method : "start()" } ) , { message : err . message , stack : err . stack } , this . prefix ) ;
2019-06-08 08:35:43 +02:00
}
2020-07-19 01:01:49 +02:00
this . emit ( "started" , addon . id ) ;
Toasts . show ( Strings . Addons . enabled . format ( { name : addon . name , version : addon . version } ) ) ;
2019-05-28 20:19:48 +02:00
}
2019-06-08 08:35:43 +02:00
2019-06-27 22:18:40 +02:00
stopPlugin ( idOrAddon ) {
const addon = typeof ( idOrAddon ) == "string" ? this . addonList . find ( p => p . id == idOrAddon ) : idOrAddon ;
if ( ! addon ) return ;
2020-07-29 21:06:54 +02:00
const plugin = addon . instance ;
2019-06-08 08:35:43 +02:00
try {
plugin . stop ( ) ;
2019-05-28 20:19:48 +02:00
}
2019-06-08 08:35:43 +02:00
catch ( err ) {
2019-06-27 22:18:40 +02:00
this . state [ addon . id ] = false ;
2020-07-19 01:01:49 +02:00
Toasts . error ( Strings . Addons . couldNotStop . format ( { name : addon . name , version : addon . version } ) ) ;
2021-03-18 22:50:47 +01:00
Logger . stacktrace ( this . name , ` ${ addon . name } v ${ addon . version } could not be started. ` , err ) ;
2021-04-06 20:09:43 +02:00
return new AddonError ( addon . name , addon . filename , Strings . Addons . enabled . format ( { method : "stop()" } ) , { message : err . message , stack : err . stack } , this . prefix ) ;
2019-06-08 08:35:43 +02:00
}
2020-07-19 01:01:49 +02:00
this . emit ( "stopped" , addon . id ) ;
Toasts . show ( Strings . Addons . disabled . format ( { name : addon . name , version : addon . version } ) ) ;
}
getPlugin ( idOrFile ) {
const addon = this . addonList . find ( c => c . id == idOrFile || c . filename == idOrFile ) ;
if ( ! addon ) return ;
2021-02-22 23:53:21 +01:00
return addon ;
2019-06-08 08:35:43 +02:00
}
setupFunctions ( ) {
2021-03-06 09:30:16 +01:00
Events . on ( "navigate" , this . onSwitch ) ;
2019-06-08 08:35:43 +02:00
this . observer . observe ( document , {
childList : true ,
subtree : true
} ) ;
2019-05-28 20:19:48 +02:00
}
2019-06-08 08:35:43 +02:00
onSwitch ( ) {
2019-06-27 22:18:40 +02:00
for ( let i = 0 ; i < this . addonList . length ; i ++ ) {
2020-07-29 21:06:54 +02:00
const plugin = this . addonList [ i ] . instance ;
2019-06-27 22:18:40 +02:00
if ( ! this . state [ this . addonList [ i ] . id ] ) continue ;
2022-09-29 07:04:22 +02:00
if ( typeof ( plugin ? . onSwitch ) === "function" ) {
2020-07-25 10:22:57 +02:00
try { plugin . onSwitch ( ) ; }
2021-03-18 22:50:47 +01:00
catch ( err ) { Logger . stacktrace ( this . name , ` Unable to fire onSwitch for ${ this . addonList [ i ] . name } v ${ this . addonList [ i ] . version } ` , err ) ; }
2019-06-08 08:35:43 +02:00
}
2019-05-28 20:19:48 +02:00
}
}
2019-06-08 08:35:43 +02:00
onMutation ( mutation ) {
2019-06-27 22:18:40 +02:00
for ( let i = 0 ; i < this . addonList . length ; i ++ ) {
2020-07-29 21:06:54 +02:00
const plugin = this . addonList [ i ] . instance ;
2019-06-27 22:18:40 +02:00
if ( ! this . state [ this . addonList [ i ] . id ] ) continue ;
2022-09-29 07:04:22 +02:00
if ( typeof plugin ? . observer === "function" ) {
2020-07-25 10:22:57 +02:00
try { plugin . observer ( mutation ) ; }
2021-03-18 22:50:47 +01:00
catch ( err ) { Logger . stacktrace ( this . name , ` Unable to fire observer for ${ this . addonList [ i ] . name } v ${ this . addonList [ i ] . version } ` , err ) ; }
2019-06-08 08:35:43 +02:00
}
2019-05-28 20:19:48 +02:00
}
}
2019-06-08 08:35:43 +02:00
} ;