2018-01-30 16:59:27 +01:00
/ * *
* BetterDiscord Content Manager Module
* Copyright ( c ) 2015 - present Jiiks / JsSucks - https : //github.com/Jiiks / https://github.com/JsSucks
* All rights reserved .
* https : //betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
* /
import Globals from './globals' ;
import { FileUtils , ClientLogger as Logger } from 'common' ;
import path from 'path' ;
2018-02-07 13:15:46 +01:00
import { Events } from 'modules' ;
2018-02-13 23:28:58 +01:00
import { ErrorEvent } from 'structs' ;
2018-02-13 17:44:07 +01:00
import { Modals } from 'ui' ;
2018-01-30 16:59:27 +01:00
2018-02-14 13:55:06 +01:00
/ * *
* Base class for external content managing
* /
2018-01-30 16:59:27 +01:00
export default class {
2018-02-14 13:55:06 +01:00
/ * *
* Any errors that happened
* returns { Array }
* /
2018-02-07 17:02:27 +01:00
static get errors ( ) {
return this . _errors || ( this . _errors = [ ] ) ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Locallly stored content
* returns { Array }
* /
2018-01-30 16:59:27 +01:00
static get localContent ( ) {
return this . _localContent ? this . _localContent : ( this . _localContent = [ ] ) ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Local path for content
* returns { String }
* /
2018-01-30 16:59:27 +01:00
static get contentPath ( ) {
return this . _contentPath ? this . _contentPath : ( this . _contentPath = Globals . getObject ( 'paths' ) . find ( path => path . id === this . pathId ) . path ) ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Load all locally stored content
* @ param { bool } suppressErrors Suppress any errors that occur during loading of content
* /
static async loadAllContent ( suppressErrors = false ) {
2018-01-30 16:59:27 +01:00
try {
2018-01-30 23:21:06 +01:00
await FileUtils . ensureDirectory ( this . contentPath ) ;
const directories = await FileUtils . listDirectory ( this . contentPath ) ;
for ( let dir of directories ) {
try {
await this . preloadContent ( dir ) ;
} catch ( err ) {
2018-02-13 23:28:58 +01:00
this . errors . push ( new ErrorEvent ( {
2018-02-07 17:02:27 +01:00
module : this . moduleName ,
message : ` Failed to load ${ dir } ` ,
err
} ) ) ;
2018-02-11 20:31:24 +01:00
2018-01-30 23:21:06 +01:00
Logger . err ( this . moduleName , err ) ;
}
}
2018-02-14 00:23:52 +01:00
if ( this . errors . length && ! suppressErrors ) {
2018-02-13 17:44:07 +01:00
Modals . error ( {
header : ` ${ this . moduleName } - ${ this . errors . length } ${ this . contentType } ${ this . errors . length !== 1 ? 's' : '' } failed to load ` ,
2018-02-07 17:02:27 +01:00
module : this . moduleName ,
type : 'err' ,
content : this . errors
} ) ;
2018-02-13 17:57:05 +01:00
this . _errors = [ ] ;
2018-02-07 17:02:27 +01:00
}
2018-01-30 23:21:06 +01:00
return this . localContent ;
} catch ( err ) {
throw err ;
}
}
2018-01-30 16:59:27 +01:00
2018-02-14 13:55:06 +01:00
/ * *
* Refresh locally stored content
2018-02-21 19:06:44 +01:00
* @ param { bool } suppressErrors Suppress any errors that occur during loading of content
2018-02-14 13:55:06 +01:00
* /
2018-02-21 19:06:44 +01:00
static async refreshContent ( suppressErrors = false ) {
2018-01-30 23:21:06 +01:00
if ( ! this . localContent . length ) return this . loadAllContent ( ) ;
try {
2018-01-30 16:59:27 +01:00
await FileUtils . ensureDirectory ( this . contentPath ) ;
const directories = await FileUtils . listDirectory ( this . contentPath ) ;
for ( let dir of directories ) {
2018-01-30 23:21:06 +01:00
// If content is already loaded this should resolve.
if ( this . getContentByDirName ( dir ) ) continue ;
2018-01-30 16:59:27 +01:00
try {
2018-01-30 23:21:06 +01:00
// Load if not
2018-01-30 16:59:27 +01:00
await this . preloadContent ( dir ) ;
} catch ( err ) {
2018-02-21 19:06:44 +01:00
// We don't want every plugin/theme to fail loading when one does
this . errors . push ( new ErrorEvent ( {
module : this . moduleName ,
message : ` Failed to load ${ dir } ` ,
err
} ) ) ;
2018-01-30 16:59:27 +01:00
Logger . err ( this . moduleName , err ) ;
}
}
2018-01-30 23:21:06 +01:00
for ( let content of this . localContent ) {
if ( directories . includes ( content . dirName ) ) continue ;
2018-02-21 19:06:44 +01:00
try {
// Plugin/theme was deleted manually, stop it and remove any reference
await this . unloadContent ( content ) ;
} catch ( err ) {
this . errors . push ( new ErrorEvent ( {
module : this . moduleName ,
2018-02-22 17:19:35 +01:00
message : ` Failed to unload ${ content . dirName } ` ,
2018-02-21 19:06:44 +01:00
err
} ) ) ;
Logger . err ( this . moduleName , err ) ;
}
2018-01-30 23:21:06 +01:00
}
2018-02-21 19:06:44 +01:00
if ( this . errors . length && ! suppressErrors ) {
Modals . error ( {
header : ` ${ this . moduleName } - ${ this . errors . length } ${ this . contentType } ${ this . errors . length !== 1 ? 's' : '' } failed to load ` ,
module : this . moduleName ,
type : 'err' ,
content : this . errors
} ) ;
this . _errors = [ ] ;
}
2018-01-30 23:21:06 +01:00
2018-02-21 19:06:44 +01:00
return this . localContent ;
2018-01-30 16:59:27 +01:00
} catch ( err ) {
throw err ;
}
}
2018-02-14 13:55:06 +01:00
/ * *
* Common loading procedure for loading content before passing it to the actual loader
* @ param { any } dirName Base directory for content
* @ param { any } reload Is content being reloaded
* @ param { any } index Index of content in { localContent }
* /
2018-01-30 16:59:27 +01:00
static async preloadContent ( dirName , reload = false , index ) {
try {
const contentPath = path . join ( this . contentPath , dirName ) ;
await FileUtils . directoryExists ( contentPath ) ;
if ( ! reload ) {
const loaded = this . localContent . find ( content => content . contentPath === contentPath ) ;
if ( loaded ) {
throw { 'message' : ` Attempted to load already loaded user content: ${ path } ` } ;
}
}
const readConfig = await this . readConfig ( contentPath ) ;
const mainPath = path . join ( contentPath , readConfig . main ) ;
2018-02-12 23:49:44 +01:00
readConfig . defaultConfig = readConfig . defaultConfig || [ ] ;
2018-01-30 16:59:27 +01:00
const userConfig = {
enabled : false ,
config : readConfig . defaultConfig
} ;
try {
const readUserConfig = await this . readUserConfig ( contentPath ) ;
2018-02-03 10:25:34 +01:00
userConfig . enabled = readUserConfig . enabled || false ;
2018-02-28 19:14:29 +01:00
for ( let category of userConfig . config ) {
const newCategory = readUserConfig . config . find ( c => c . category === category . category ) ;
2018-02-04 21:17:22 +01:00
2018-02-28 19:14:29 +01:00
for ( let setting of category . settings ) {
setting . path = contentPath ;
if ( ! newCategory ) continue ;
2018-02-21 16:58:45 +01:00
const newSetting = newCategory . settings . find ( s => s . id === setting . id ) ;
2018-02-28 19:14:29 +01:00
if ( ! newSetting ) continue ;
2018-02-04 21:17:22 +01:00
2018-02-21 16:58:45 +01:00
setting . value = newSetting . value ;
2018-02-28 19:14:29 +01:00
}
}
2018-02-11 20:31:24 +01:00
userConfig . css = readUserConfig . css || null ;
2018-02-03 10:25:34 +01:00
} catch ( err ) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/
2018-02-04 21:17:22 +01:00
2018-02-03 10:25:34 +01:00
}
2018-01-30 16:59:27 +01:00
const configs = {
defaultConfig : readConfig . defaultConfig ,
2018-02-15 18:09:06 +01:00
schemes : readConfig . configSchemes ,
2018-01-30 16:59:27 +01:00
userConfig
2018-02-21 16:58:45 +01:00
} ;
2018-01-30 16:59:27 +01:00
const paths = {
contentPath ,
dirName ,
mainPath
2018-02-21 16:58:45 +01:00
} ;
2018-01-30 16:59:27 +01:00
2018-02-28 20:34:12 +01:00
const content = await this . loadContent ( paths , configs , readConfig . info , readConfig . main , readConfig . dependencies , readConfig . permissions ) ;
if ( ! content ) return null ;
2018-02-21 18:46:27 +01:00
if ( ! reload && this . getContentById ( content . id ) )
throw { message : ` A ${ this . contentType } with the ID ${ content . id } already exists. ` } ;
2018-01-31 09:17:15 +01:00
if ( reload ) this . localContent [ index ] = content ;
else this . localContent . push ( content ) ;
2018-01-30 16:59:27 +01:00
return content ;
} catch ( err ) {
throw err ;
}
}
2018-02-04 21:17:22 +01:00
2018-02-21 18:46:27 +01:00
/ * *
* Unload content
* @ param { any } content Content to unload
* @ param { bool } reload Whether to reload the content after
* /
static async unloadContent ( content , reload ) {
content = this . findContent ( content ) ;
if ( ! content ) throw { message : ` Could not find a ${ this . contentType } from ${ content } . ` } ;
try {
if ( content . enabled && content . disable ) content . disable ( false ) ;
if ( content . enabled && content . stop ) content . stop ( false ) ;
if ( content . onunload ) content . onunload ( reload ) ;
if ( content . onUnload ) content . onUnload ( reload ) ;
const index = this . getContentIndex ( content ) ;
delete window . require . cache [ window . require . resolve ( content . paths . mainPath ) ] ;
if ( reload ) {
const newcontent = await this . preloadContent ( content . dirName , true , index ) ;
if ( newcontent . enabled && newcontent . start ) newcontent . start ( false ) ;
return newcontent ;
} else this . localContent . splice ( index , 1 ) ;
} catch ( err ) {
Logger . err ( this . moduleName , err ) ;
throw err ;
}
}
/ * *
* Reload content
* @ param { any } content Content to reload
* /
static async reloadContent ( content ) {
return this . unloadContent ( content , true ) ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Read content config file
* @ param { any } configPath Config file path
* /
2018-01-30 16:59:27 +01:00
static async readConfig ( configPath ) {
configPath = path . resolve ( configPath , 'config.json' ) ;
return FileUtils . readJsonFromFile ( configPath ) ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Read content user config file
* @ param { any } configPath User config file path
* /
2018-01-30 16:59:27 +01:00
static async readUserConfig ( configPath ) {
configPath = path . resolve ( configPath , 'user.config.json' ) ;
return FileUtils . readJsonFromFile ( configPath ) ;
}
2018-02-21 18:46:27 +01:00
/ * *
* Checks if the passed object is an instance of this content type .
* @ param { any } content Object to check
* /
static isThisContent ( content ) {
return false ;
}
2018-02-14 13:55:06 +01:00
/ * *
* Wildcard content finder
* @ param { any } wild Content name | id | path | dirname
2018-02-21 18:46:27 +01:00
* @ param { bool } nonunique Allow searching attributes that may not be unique
2018-02-14 13:55:06 +01:00
* /
2018-02-21 18:46:27 +01:00
static findContent ( wild , nonunique ) {
if ( this . isThisContent ( wild ) ) return wild ;
let content ;
2018-02-22 17:19:35 +01:00
content = this . getContentById ( wild ) ; if ( content ) return content ;
content = this . getContentByDirName ( wild ) ; if ( content ) return content ;
content = this . getContentByPath ( wild ) ; if ( content ) return content ;
content = this . getContentByName ( wild ) ; if ( content && nonunique ) return content ;
2018-01-30 23:21:06 +01:00
}
static getContentIndex ( content ) { return this . localContent . findIndex ( c => c === content ) }
static getContentByName ( name ) { return this . localContent . find ( c => c . name === name ) }
static getContentById ( id ) { return this . localContent . find ( c => c . id === id ) }
static getContentByPath ( path ) { return this . localContent . find ( c => c . contentPath === path ) }
static getContentByDirName ( dirName ) { return this . localContent . find ( c => c . dirName === dirName ) }
2018-02-14 13:55:06 +01:00
/ * *
* Wait for content to load
* @ param { any } content _id
* /
2018-02-12 23:49:44 +01:00
static waitForContent ( content _id ) {
return new Promise ( ( resolve , reject ) => {
const check = ( ) => {
const content = this . getContentById ( content _id ) ;
if ( content ) return resolve ( content ) ;
setTimeout ( check , 100 ) ;
} ;
check ( ) ;
} ) ;
}
2018-02-05 15:33:30 +01:00
}