2021-03-06 09:30:16 +01:00
import Logger from "common/logger" ;
2019-06-27 22:18:40 +02:00
import Settings from "./settingsmanager" ;
import Events from "./emitter" ;
import DataStore from "./datastore" ;
import AddonError from "../structs/addonerror" ;
import Toasts from "../ui/toasts" ;
2019-06-30 07:32:14 +02:00
import DiscordModules from "./discordmodules" ;
import Strings from "./strings" ;
import AddonEditor from "../ui/misc/addoneditor" ;
2020-07-18 04:24:20 +02:00
import FloatingWindows from "../ui/floatingwindows" ;
2019-06-30 07:32:14 +02:00
const React = DiscordModules . React ;
2019-06-27 22:18:40 +02:00
const path = require ( "path" ) ;
const fs = require ( "fs" ) ;
2021-04-16 00:03:59 +02:00
const shell = require ( "electron" ) . shell ;
const openItem = shell . openItem || shell . openPath ;
2019-06-27 22:18:40 +02:00
2020-02-28 01:00:12 +01:00
const splitRegex = /[^\S\r\n]*?\r?(?:\r\n|\n)[^\S\r\n]*?\*[^\S\r\n]?/ ;
2019-06-27 22:18:40 +02:00
const escapedAtRegex = /^\\@/ ;
const stripBOM = function ( fileContent ) {
if ( fileContent . charCodeAt ( 0 ) === 0xFEFF ) {
fileContent = fileContent . slice ( 1 ) ;
}
return fileContent ;
} ;
export default class AddonManager {
get name ( ) { return "" ; }
get extension ( ) { return "" ; }
2020-10-04 02:50:31 +02:00
get duplicatePattern ( ) { return /./ ; }
2019-06-27 22:18:40 +02:00
get addonFolder ( ) { return "" ; }
2019-06-30 07:32:14 +02:00
get language ( ) { return "" ; }
2019-06-27 22:18:40 +02:00
get prefix ( ) { return "addon" ; }
emit ( event , ... args ) { return Events . emit ( ` ${ this . prefix } - ${ event } ` , ... args ) ; }
constructor ( ) {
this . timeCache = { } ;
this . addonList = [ ] ;
this . state = { } ;
2020-11-03 02:47:08 +01:00
this . windows = new Set ( ) ;
2019-06-27 22:18:40 +02:00
}
2021-03-18 22:50:47 +01:00
initialize ( ) {
return this . loadAllAddons ( ) ;
2019-06-27 22:18:40 +02:00
}
// Subclasses should overload this and modify the addon object as needed to fully load it
initializeAddon ( ) { return ; }
startAddon ( ) { return ; }
stopAddon ( ) { return ; }
loadState ( ) {
const saved = DataStore . getData ( ` ${ this . prefix } s ` ) ;
if ( ! saved ) return ;
Object . assign ( this . state , saved ) ;
}
saveState ( ) {
DataStore . setData ( ` ${ this . prefix } s ` , this . state ) ;
}
watchAddons ( ) {
2020-10-05 22:00:33 +02:00
if ( this . watcher ) return Logger . err ( this . name , ` Already watching ${ this . prefix } addons. ` ) ;
2019-06-27 22:18:40 +02:00
Logger . log ( this . name , ` Starting to watch ${ this . prefix } addons. ` ) ;
this . watcher = fs . watch ( this . addonFolder , { persistent : false } , async ( eventType , filename ) => {
2022-09-25 21:29:17 +02:00
// console.log("watcher", eventType, filename, !eventType || !filename, !filename.endsWith(this.extension));
2020-10-04 02:50:31 +02:00
if ( ! eventType || ! filename ) return ;
2022-09-25 21:29:17 +02:00
// console.log(eventType, filename)
2020-10-04 02:50:31 +02:00
const absolutePath = path . resolve ( this . addonFolder , filename ) ;
if ( ! filename . endsWith ( this . extension ) ) {
// Lets check to see if this filename has the duplicated file pattern `something(1).ext`
const match = filename . match ( this . duplicatePattern ) ;
2020-10-04 02:59:01 +02:00
if ( ! match ) return ;
2020-10-04 02:50:31 +02:00
const ext = match [ 0 ] ;
const truncated = filename . replace ( ext , "" ) ;
const newFilename = truncated + this . extension ;
// If this file already exists, give a warning and move on.
2020-10-04 02:59:01 +02:00
if ( fs . existsSync ( newFilename ) ) {
2021-07-26 00:09:18 +02:00
Logger . warn ( this . name , ` Duplicate files found: ${ filename } and ${ newFilename } ` ) ;
2020-10-04 02:59:01 +02:00
return ;
2020-10-04 02:50:31 +02:00
}
// Rename the file and let it go on
2021-07-26 00:09:18 +02:00
try {
fs . renameSync ( absolutePath , path . resolve ( this . addonFolder , newFilename ) ) ;
}
catch ( error ) {
Logger . err ( this . name , ` Could not rename file: ${ filename } ${ newFilename } ` , error ) ;
}
2020-10-04 02:50:31 +02:00
}
2022-09-25 21:29:17 +02:00
// console.log("watcher", "before promise");
2020-07-18 04:24:20 +02:00
await new Promise ( r => setTimeout ( r , 100 ) ) ;
try {
2020-10-04 02:50:31 +02:00
const stats = fs . statSync ( absolutePath ) ;
2022-09-25 21:29:17 +02:00
// console.log("watcher", stats);
2020-07-18 04:24:20 +02:00
if ( ! stats . isFile ( ) ) return ;
if ( ! stats || ! stats . mtime || ! stats . mtime . getTime ( ) ) return ;
if ( typeof ( stats . mtime . getTime ( ) ) !== "number" ) return ;
if ( this . timeCache [ filename ] == stats . mtime . getTime ( ) ) return ;
this . timeCache [ filename ] = stats . mtime . getTime ( ) ;
if ( eventType == "rename" ) this . loadAddon ( filename , true ) ;
if ( eventType == "change" ) this . reloadAddon ( filename , true ) ;
}
2019-06-27 22:18:40 +02:00
catch ( err ) {
2022-09-25 21:29:17 +02:00
// window.watcherError = err;
// console.log("watcher", err);
// console.dir(err);
if ( err . code !== "ENOENT" && ! err ? . message . startsWith ( "ENOENT" ) ) return ;
2019-06-27 22:18:40 +02:00
delete this . timeCache [ filename ] ;
this . unloadAddon ( filename , true ) ;
}
} ) ;
}
unwatchAddons ( ) {
if ( ! this . watcher ) return Logger . error ( this . name , ` Was not watching ${ this . prefix } addons. ` ) ;
this . watcher . close ( ) ;
delete this . watcher ;
Logger . log ( this . name , ` No longer watching ${ this . prefix } addons. ` ) ;
}
2022-08-09 19:28:50 +02:00
extractMeta ( fileContent , filename ) {
2019-06-27 22:18:40 +02:00
const firstLine = fileContent . split ( "\n" ) [ 0 ] ;
2022-08-09 19:28:50 +02:00
const hasOldMeta = firstLine . includes ( "//META" ) && firstLine . includes ( "*//" ) ;
if ( hasOldMeta ) return this . parseOldMeta ( fileContent , filename ) ;
2019-06-27 22:18:40 +02:00
const hasNewMeta = firstLine . includes ( "/**" ) ;
if ( hasNewMeta ) return this . parseNewMeta ( fileContent ) ;
2022-08-09 19:28:50 +02:00
throw new AddonError ( filename , filename , Strings . Addons . metaNotFound , { message : "" , stack : fileContent } , this . prefix ) ;
2019-06-27 22:18:40 +02:00
}
2022-08-09 19:28:50 +02:00
parseOldMeta ( fileContent , filename ) {
2019-06-27 22:18:40 +02:00
const meta = fileContent . split ( "\n" ) [ 0 ] ;
const metaData = meta . substring ( meta . lastIndexOf ( "//META" ) + 6 , meta . lastIndexOf ( "*//" ) ) ;
2022-10-02 09:34:34 +02:00
let parsed = null ;
try {
parsed = JSON . parse ( metaData ) ;
}
catch ( err ) {
throw new AddonError ( filename , filename , Strings . Addons . metaError , err , this . prefix ) ;
}
if ( ! parsed || ! parsed . name ) throw new AddonError ( filename , filename , Strings . Addons . missingNameData , { message : "" , stack : meta } , this . prefix ) ;
2020-02-28 01:00:12 +01:00
parsed . format = "json" ;
2019-06-27 22:18:40 +02:00
return parsed ;
}
parseNewMeta ( fileContent ) {
const block = fileContent . split ( "/**" , 2 ) [ 1 ] . split ( "*/" , 1 ) [ 0 ] ;
const out = { } ;
let field = "" ;
let accum = "" ;
for ( const line of block . split ( splitRegex ) ) {
if ( line . length === 0 ) continue ;
if ( line . charAt ( 0 ) === "@" && line . charAt ( 1 ) !== " " ) {
2022-08-02 02:52:07 +02:00
out [ field ] = accum . trim ( ) ;
2019-06-27 22:18:40 +02:00
const l = line . indexOf ( " " ) ;
2022-06-25 09:50:55 +02:00
field = line . substring ( 1 , l ) ;
accum = line . substring ( l + 1 ) ;
2019-06-27 22:18:40 +02:00
}
else {
accum += " " + line . replace ( "\\n" , "\n" ) . replace ( escapedAtRegex , "@" ) ;
}
}
out [ field ] = accum . trim ( ) ;
delete out [ "" ] ;
2020-02-28 01:00:12 +01:00
out . format = "jsdoc" ;
2019-06-27 22:18:40 +02:00
return out ;
}
2022-07-08 16:42:04 +02:00
// Subclasses should overload this and modify the addon using the fileContent as needed to "require()"" the file
requireAddon ( filename ) {
let fileContent = fs . readFileSync ( filename , "utf8" ) ;
fileContent = stripBOM ( fileContent ) ;
const stats = fs . statSync ( filename ) ;
2022-08-09 19:28:50 +02:00
const addon = this . extractMeta ( fileContent , path . basename ( filename ) ) ;
2022-07-08 16:42:04 +02:00
if ( ! addon . author ) addon . author = Strings . Addons . unknownAuthor ;
if ( ! addon . version ) addon . version = "???" ;
if ( ! addon . description ) addon . description = Strings . Addons . noDescription ;
// if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix);
2022-08-09 19:28:50 +02:00
addon . id = addon . name || path . basename ( filename ) ;
2022-07-08 16:42:04 +02:00
addon . slug = path . basename ( filename ) . replace ( this . extension , "" ) . replace ( / /g , "-" ) ;
addon . filename = path . basename ( filename ) ;
addon . added = stats . atimeMs ;
addon . modified = stats . mtimeMs ;
addon . size = stats . size ;
addon . fileContent = fileContent ;
2022-09-29 07:04:22 +02:00
if ( this . addonList . find ( c => c . id == addon . id ) ) throw new AddonError ( addon . name , filename , Strings . Addons . alreadyExists . format ( { type : this . prefix , name : addon . name } ) , this . prefix ) ;
this . addonList . push ( addon ) ;
2022-07-08 16:42:04 +02:00
return addon ;
2019-06-27 22:18:40 +02:00
}
// Subclasses should use the return (if not AddonError) and push to this.addonList
2021-03-18 22:50:47 +01:00
loadAddon ( filename , shouldToast = false ) {
2019-06-27 22:18:40 +02:00
if ( typeof ( filename ) === "undefined" ) return ;
2022-08-09 19:28:50 +02:00
let addon ;
try {
addon = this . requireAddon ( path . resolve ( this . addonFolder , filename ) ) ;
}
catch ( e ) {
2022-09-29 07:04:22 +02:00
const partialAddon = this . addonList . find ( c => c . filename == filename ) ;
if ( partialAddon ) {
partialAddon . partial = true ;
this . state [ partialAddon . id ] = false ;
2022-10-10 08:32:57 +02:00
this . emit ( "loaded" , partialAddon ) ;
2022-09-29 07:04:22 +02:00
}
2022-08-09 19:28:50 +02:00
return e ;
}
2022-09-29 07:04:22 +02:00
2020-10-06 23:44:10 +02:00
2019-06-27 22:18:40 +02:00
const error = this . initializeAddon ( addon ) ;
2022-09-29 07:04:22 +02:00
if ( error ) {
this . state [ addon . id ] = false ;
addon . partial = true ;
2022-10-10 08:32:57 +02:00
this . emit ( "loaded" , addon ) ;
2022-09-29 07:04:22 +02:00
return error ;
}
2020-10-06 23:44:10 +02:00
2022-10-14 05:42:05 +02:00
if ( shouldToast ) Toasts . success ( Strings . Addons . wasUnloaded . format ( { name : addon . name , version : addon . version } ) ) ;
2022-10-10 08:32:57 +02:00
this . emit ( "loaded" , addon ) ;
2020-10-05 22:00:33 +02:00
2019-06-27 22:18:40 +02:00
if ( ! this . state [ addon . id ] ) return this . state [ addon . id ] = false ;
return this . startAddon ( addon ) ;
}
unloadAddon ( idOrFileOrAddon , shouldToast = true , isReload = false ) {
const addon = typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
2022-09-25 21:29:17 +02:00
// console.log("watcher", "unloadAddon", idOrFileOrAddon, addon);
2019-06-27 22:18:40 +02:00
if ( ! addon ) return false ;
if ( this . state [ addon . id ] ) isReload ? this . stopAddon ( addon ) : this . disableAddon ( addon ) ;
2022-07-08 16:42:04 +02:00
2019-06-27 22:18:40 +02:00
this . addonList . splice ( this . addonList . indexOf ( addon ) , 1 ) ;
2022-10-10 08:32:57 +02:00
this . emit ( "unloaded" , addon ) ;
2022-10-14 05:42:05 +02:00
if ( shouldToast ) Toasts . success ( Strings . Addons . wasUnloaded . format ( { name : addon . name } ) ) ;
2019-06-27 22:18:40 +02:00
return true ;
}
2021-03-18 22:50:47 +01:00
reloadAddon ( idOrFileOrAddon , shouldToast = true ) {
2019-06-27 22:18:40 +02:00
const addon = typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
const didUnload = this . unloadAddon ( addon , shouldToast , true ) ;
2020-10-06 23:44:10 +02:00
if ( addon && ! didUnload ) return didUnload ;
2021-03-18 22:50:47 +01:00
return this . loadAddon ( addon ? addon . filename : idOrFileOrAddon , shouldToast ) ;
2019-06-27 22:18:40 +02:00
}
isLoaded ( idOrFile ) {
const addon = this . addonList . find ( c => c . id == idOrFile || c . filename == idOrFile ) ;
if ( ! addon ) return false ;
return true ;
}
isEnabled ( idOrFile ) {
const addon = this . addonList . find ( c => c . id == idOrFile || c . filename == idOrFile ) ;
if ( ! addon ) return false ;
return this . state [ addon . id ] ;
}
2020-07-19 01:01:49 +02:00
getAddon ( idOrFile ) {
return this . addonList . find ( c => c . id == idOrFile || c . filename == idOrFile ) ;
}
2019-06-27 22:18:40 +02:00
enableAddon ( idOrAddon ) {
const addon = typeof ( idOrAddon ) == "string" ? this . addonList . find ( p => p . id == idOrAddon ) : idOrAddon ;
2022-09-29 07:04:22 +02:00
if ( ! addon || addon . partial ) return ;
2019-06-27 22:18:40 +02:00
if ( this . state [ addon . id ] ) return ;
this . state [ addon . id ] = true ;
this . startAddon ( addon ) ;
this . saveState ( ) ;
}
disableAddon ( idOrAddon ) {
const addon = typeof ( idOrAddon ) == "string" ? this . addonList . find ( p => p . id == idOrAddon ) : idOrAddon ;
2022-09-29 07:04:22 +02:00
if ( ! addon || addon . partial ) return ;
2019-06-27 22:18:40 +02:00
if ( ! this . state [ addon . id ] ) return ;
this . state [ addon . id ] = false ;
this . stopAddon ( addon ) ;
this . saveState ( ) ;
}
toggleAddon ( id ) {
if ( this . state [ id ] ) this . disableAddon ( id ) ;
else this . enableAddon ( id ) ;
}
loadNewAddons ( ) {
const files = fs . readdirSync ( this . addonFolder ) ;
const removed = this . addonList . filter ( t => ! files . includes ( t . filename ) ) . map ( c => c . id ) ;
const added = files . filter ( f => ! this . addonList . find ( t => t . filename == f ) && f . endsWith ( this . extension ) && fs . statSync ( path . resolve ( this . addonFolder , f ) ) . isFile ( ) ) ;
return { added , removed } ;
}
updateList ( ) {
const results = this . loadNewAddons ( ) ;
for ( const filename of results . added ) this . loadAddon ( filename ) ;
for ( const name of results . removed ) this . unloadAddon ( name ) ;
}
2021-03-18 22:50:47 +01:00
loadAllAddons ( ) {
2019-06-27 22:18:40 +02:00
this . loadState ( ) ;
const errors = [ ] ;
const files = fs . readdirSync ( this . addonFolder ) ;
for ( const filename of files ) {
2020-10-04 02:50:31 +02:00
const absolutePath = path . resolve ( this . addonFolder , filename ) ;
2020-10-06 23:44:10 +02:00
const stats = fs . statSync ( absolutePath ) ;
if ( ! stats || ! stats . isFile ( ) ) continue ;
this . timeCache [ filename ] = stats . mtime . getTime ( ) ;
2020-10-04 02:50:31 +02:00
if ( ! filename . endsWith ( this . extension ) ) {
// Lets check to see if this filename has the duplicated file pattern `something(1).ext`
const match = filename . match ( this . duplicatePattern ) ;
if ( ! match ) continue ;
const ext = match [ 0 ] ;
const truncated = filename . replace ( ext , "" ) ;
const newFilename = truncated + this . extension ;
// If this file already exists, give a warning and move on.
2020-10-04 02:59:01 +02:00
if ( fs . existsSync ( newFilename ) ) {
2020-10-04 02:50:31 +02:00
Logger . warn ( "AddonManager" , ` Duplicate files found: ${ filename } and ${ newFilename } ` ) ;
continue ;
}
// Rename the file and let it go on
fs . renameSync ( absolutePath , path . resolve ( this . addonFolder , newFilename ) ) ;
}
2021-03-18 22:50:47 +01:00
const addon = this . loadAddon ( filename , false ) ;
2019-06-27 22:18:40 +02:00
if ( addon instanceof AddonError ) errors . push ( addon ) ;
}
this . saveState ( ) ;
2022-08-07 23:26:47 +02:00
this . watchAddons ( ) ;
2019-06-27 22:18:40 +02:00
return errors ;
}
2019-06-30 07:32:14 +02:00
deleteAddon ( idOrFileOrAddon ) {
const addon = typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
2022-09-25 21:29:17 +02:00
// console.log(path.resolve(this.addonFolder, addon.filename), fs.unlinkSync)
2019-06-30 07:32:14 +02:00
return fs . unlinkSync ( path . resolve ( this . addonFolder , addon . filename ) ) ;
}
saveAddon ( idOrFileOrAddon , content ) {
const addon = typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
return fs . writeFileSync ( path . resolve ( this . addonFolder , addon . filename ) , content ) ;
}
editAddon ( idOrFileOrAddon , system ) {
const addon = typeof ( idOrFileOrAddon ) == "string" ? this . addonList . find ( c => c . id == idOrFileOrAddon || c . filename == idOrFileOrAddon ) : idOrFileOrAddon ;
const fullPath = path . resolve ( this . addonFolder , addon . filename ) ;
if ( typeof ( system ) == "undefined" ) system = Settings . get ( "settings" , "addons" , "editAction" ) == "system" ;
2021-04-16 00:03:59 +02:00
if ( system ) return openItem ( ` ${ fullPath } ` ) ;
2019-06-30 07:32:14 +02:00
return this . openDetached ( addon ) ;
}
openDetached ( addon ) {
const fullPath = path . resolve ( this . addonFolder , addon . filename ) ;
const content = fs . readFileSync ( fullPath ) . toString ( ) ;
2020-11-03 02:47:08 +01:00
if ( this . windows . has ( fullPath ) ) return ;
this . windows . add ( fullPath ) ;
2019-06-30 07:32:14 +02:00
const editorRef = React . createRef ( ) ;
const editor = React . createElement ( AddonEditor , {
2020-11-03 02:47:08 +01:00
id : "bd-floating-editor-" + addon . id ,
2019-06-30 07:32:14 +02:00
ref : editorRef ,
content : content ,
save : this . saveAddon . bind ( this , addon ) ,
openNative : this . editAddon . bind ( this , addon , true ) ,
language : this . language
} ) ;
2020-07-18 04:24:20 +02:00
FloatingWindows . open ( {
2019-06-30 07:32:14 +02:00
onClose : ( ) => {
2020-11-03 02:47:08 +01:00
this . windows . delete ( fullPath ) ;
2019-06-30 07:32:14 +02:00
} ,
onResize : ( ) => {
if ( ! editorRef || ! editorRef . current || ! editorRef . current . resize ) return ;
editorRef . current . resize ( ) ;
} ,
title : addon . name ,
2020-11-03 02:47:08 +01:00
id : "bd-floating-window-" + addon . id ,
2019-06-30 07:32:14 +02:00
className : "floating-addon-window" ,
height : 470 ,
width : 410 ,
center : true ,
resizable : true ,
children : editor ,
confirmClose : ( ) => {
if ( ! editorRef || ! editorRef . current ) return false ;
return editorRef . current . hasUnsavedChanges ;
} ,
confirmationText : Strings . Addons . confirmationText . format ( { name : addon . name } )
} ) ;
}
2019-06-27 22:18:40 +02:00
}