2024-12-20 00:48:18 +01:00
const Path = require ( 'path' )
const Logger = require ( '../Logger' )
2024-12-21 00:21:00 +01:00
const Database = require ( '../Database' )
2024-12-20 00:48:18 +01:00
const PluginAbstract = require ( '../PluginAbstract' )
2024-12-21 17:20:09 +01:00
const fsExtra = require ( '../libs/fsExtra' )
2024-12-21 20:26:42 +01:00
const { isUUID , parseSemverStrict } = require ( '../utils' )
2024-12-20 00:48:18 +01:00
2024-12-21 00:21:00 +01:00
/ * *
* @ typedef PluginContext
* @ property { import ( '../../server/Logger' ) } Logger
* @ property { import ( '../../server/Database' ) } Database
* /
2024-12-20 00:48:18 +01:00
class PluginManager {
constructor ( ) {
this . plugins = [ ]
}
2024-12-21 17:20:09 +01:00
get pluginMetadataPath ( ) {
return Path . posix . join ( global . MetadataPath , 'plugins' )
}
2024-12-21 00:21:00 +01:00
get pluginData ( ) {
return this . plugins . map ( ( plugin ) => plugin . manifest )
2024-12-20 00:48:18 +01:00
}
2024-12-21 00:21:00 +01:00
/ * *
* @ returns { PluginContext }
* /
2024-12-20 00:48:18 +01:00
get pluginContext ( ) {
return {
2024-12-21 00:21:00 +01:00
Logger ,
Database
2024-12-20 00:48:18 +01:00
}
}
/ * *
2024-12-21 20:26:42 +01:00
* Validate and load a plugin from a directory
* TODO : Validatation
2024-12-20 00:48:18 +01:00
*
2024-12-21 20:26:42 +01:00
* @ param { string } dirname
2024-12-20 00:48:18 +01:00
* @ param { string } pluginPath
* @ returns { Promise < { manifest : Object , contents : PluginAbstract } > }
* /
2024-12-21 20:26:42 +01:00
async loadPlugin ( dirname , pluginPath ) {
2024-12-21 17:20:09 +01:00
const pluginFiles = await fsExtra . readdir ( pluginPath , { withFileTypes : true } ) . then ( ( files ) => files . filter ( ( file ) => ! file . isDirectory ( ) ) )
2024-12-20 00:48:18 +01:00
if ( ! pluginFiles . length ) {
Logger . error ( ` No files found in plugin ${ pluginPath } ` )
return null
}
const manifestFile = pluginFiles . find ( ( file ) => file . name === 'manifest.json' )
if ( ! manifestFile ) {
Logger . error ( ` No manifest found for plugin ${ pluginPath } ` )
return null
}
const indexFile = pluginFiles . find ( ( file ) => file . name === 'index.js' )
if ( ! indexFile ) {
Logger . error ( ` No index file found for plugin ${ pluginPath } ` )
return null
}
let manifestJson = null
try {
2024-12-21 17:20:09 +01:00
manifestJson = await fsExtra . readFile ( Path . join ( pluginPath , manifestFile . name ) , 'utf8' ) . then ( ( data ) => JSON . parse ( data ) )
2024-12-20 00:48:18 +01:00
} catch ( error ) {
Logger . error ( ` Error parsing manifest file for plugin ${ pluginPath } ` , error )
return null
}
// TODO: Validate manifest json
2024-12-21 20:26:42 +01:00
if ( ! isUUID ( manifestJson . id ) ) {
Logger . error ( ` Invalid plugin ID in manifest for plugin ${ pluginPath } ` )
return null
}
if ( ! parseSemverStrict ( manifestJson . version ) ) {
Logger . error ( ` Invalid plugin version in manifest for plugin ${ pluginPath } ` )
return null
}
// TODO: Enforcing plugin name to be the same as the directory name? Ensures plugins are identifiable in the file system. May have issues with unicode characters.
if ( dirname !== manifestJson . name ) {
Logger . error ( ` Plugin directory name " ${ dirname } " does not match manifest name " ${ manifestJson . name } " ` )
return null
}
2024-12-20 00:48:18 +01:00
2024-12-21 00:21:00 +01:00
let pluginInstance = null
2024-12-20 00:48:18 +01:00
try {
2024-12-21 00:21:00 +01:00
pluginInstance = require ( Path . join ( pluginPath , indexFile . name ) )
2024-12-20 00:48:18 +01:00
} catch ( error ) {
Logger . error ( ` Error loading plugin ${ pluginPath } ` , error )
return null
}
2024-12-21 00:21:00 +01:00
if ( typeof pluginInstance . init !== 'function' ) {
Logger . error ( ` Plugin ${ pluginPath } does not have an init function ` )
return null
}
2024-12-20 00:48:18 +01:00
return {
manifest : manifestJson ,
2024-12-21 00:21:00 +01:00
instance : pluginInstance
2024-12-20 00:48:18 +01:00
}
}
2024-12-21 20:26:42 +01:00
/ * *
* Get all plugins from the / metadata / plugins directory
* /
async getPluginsFromFileSystem ( ) {
2024-12-21 17:20:09 +01:00
await fsExtra . ensureDir ( this . pluginMetadataPath )
2024-12-21 20:26:42 +01:00
// Get all directories in the plugins directory
2024-12-21 17:20:09 +01:00
const pluginDirs = await fsExtra . readdir ( this . pluginMetadataPath , { withFileTypes : true , recursive : true } ) . then ( ( files ) => files . filter ( ( file ) => file . isDirectory ( ) ) )
2024-12-20 00:48:18 +01:00
2024-12-21 20:26:42 +01:00
const pluginsFound = [ ]
2024-12-20 00:48:18 +01:00
for ( const pluginDir of pluginDirs ) {
2024-12-21 20:26:42 +01:00
Logger . debug ( ` [PluginManager] Checking if directory " ${ pluginDir . name } " is a plugin ` )
const plugin = await this . loadPlugin ( pluginDir . name , Path . join ( this . pluginMetadataPath , pluginDir . name ) )
2024-12-20 00:48:18 +01:00
if ( plugin ) {
2024-12-21 20:26:42 +01:00
Logger . debug ( ` [PluginManager] Found plugin " ${ plugin . manifest . name } " ` )
pluginsFound . push ( plugin )
}
}
return pluginsFound
}
/ * *
* Load plugins from the / metadata / plugins directory and update the database
* /
async loadPlugins ( ) {
const pluginsFound = await this . getPluginsFromFileSystem ( )
const existingPlugins = await Database . pluginModel . findAll ( )
// Add new plugins or update existing plugins
for ( const plugin of pluginsFound ) {
const existingPlugin = existingPlugins . find ( ( p ) => p . id === plugin . manifest . id )
if ( existingPlugin ) {
// TODO: Should automatically update?
if ( existingPlugin . version !== plugin . manifest . version ) {
Logger . info ( ` [PluginManager] Updating plugin " ${ plugin . manifest . name } " version from " ${ existingPlugin . version } " to version " ${ plugin . manifest . version } " ` )
await existingPlugin . update ( { version : plugin . manifest . version , isMissing : false } )
} else if ( existingPlugin . isMissing ) {
Logger . info ( ` [PluginManager] Plugin " ${ plugin . manifest . name } " was missing but is now found ` )
await existingPlugin . update ( { isMissing : false } )
} else {
Logger . debug ( ` [PluginManager] Plugin " ${ plugin . manifest . name } " already exists in the database with version " ${ plugin . manifest . version } " ` )
}
} else {
await Database . pluginModel . create ( {
id : plugin . manifest . id ,
name : plugin . manifest . name ,
version : plugin . manifest . version
} )
Logger . info ( ` [PluginManager] Added plugin " ${ plugin . manifest . name } " to the database ` )
2024-12-20 00:48:18 +01:00
}
}
2024-12-21 20:26:42 +01:00
// Mark missing plugins
for ( const plugin of existingPlugins ) {
const foundPlugin = pluginsFound . find ( ( p ) => p . manifest . id === plugin . id )
if ( ! foundPlugin && ! plugin . isMissing ) {
Logger . info ( ` [PluginManager] Plugin " ${ plugin . name } " not found or invalid - marking as missing ` )
await plugin . update ( { isMissing : true } )
}
}
this . plugins = pluginsFound
2024-12-20 00:48:18 +01:00
}
2024-12-21 20:26:42 +01:00
/ * *
* Load and initialize all plugins
* /
2024-12-20 00:48:18 +01:00
async init ( ) {
await this . loadPlugins ( )
for ( const plugin of this . plugins ) {
2024-12-21 00:21:00 +01:00
if ( plugin . instance . init ) {
2024-12-20 00:48:18 +01:00
Logger . info ( ` [PluginManager] Initializing plugin ${ plugin . manifest . name } ` )
2024-12-21 00:21:00 +01:00
plugin . instance . init ( this . pluginContext )
2024-12-20 00:48:18 +01:00
}
}
}
onAction ( pluginSlug , actionName , target , data ) {
const plugin = this . plugins . find ( ( plugin ) => plugin . manifest . slug === pluginSlug )
if ( ! plugin ) {
Logger . error ( ` [PluginManager] Plugin ${ pluginSlug } not found ` )
return
}
const pluginExtension = plugin . manifest . extensions . find ( ( extension ) => extension . name === actionName )
if ( ! pluginExtension ) {
Logger . error ( ` [PluginManager] Extension ${ actionName } not found for plugin ${ plugin . manifest . name } ` )
return
}
2024-12-21 00:21:00 +01:00
if ( plugin . instance . onAction ) {
2024-12-20 00:48:18 +01:00
Logger . info ( ` [PluginManager] Calling onAction for plugin ${ plugin . manifest . name } ` )
2024-12-21 00:21:00 +01:00
plugin . instance . onAction ( this . pluginContext , actionName , target , data )
}
}
onConfigSave ( pluginSlug , config ) {
const plugin = this . plugins . find ( ( plugin ) => plugin . manifest . slug === pluginSlug )
if ( ! plugin ) {
Logger . error ( ` [PluginManager] Plugin ${ pluginSlug } not found ` )
return
}
if ( plugin . instance . onConfigSave ) {
Logger . info ( ` [PluginManager] Calling onConfigSave for plugin ${ plugin . manifest . name } ` )
plugin . instance . onConfigSave ( this . pluginContext , config )
2024-12-20 00:48:18 +01:00
}
}
pluginExists ( name ) {
return this . plugins . some ( ( plugin ) => plugin . name === name )
}
registerPlugin ( plugin ) {
if ( ! plugin . name ) {
throw new Error ( 'The plugin name and package are required' )
}
if ( this . pluginExists ( plugin . name ) ) {
throw new Error ( ` Cannot add existing plugin ${ plugin . name } ` )
}
try {
// Try to load the plugin
2024-12-21 17:20:09 +01:00
const pluginPath = Path . join ( this . pluginMetadataPath , plugin . name )
2024-12-20 00:48:18 +01:00
const packageContents = require ( pluginPath )
console . log ( 'packageContents' , packageContents )
packageContents . init ( )
this . plugins . push ( packageContents )
} catch ( error ) {
console . log ( ` Cannot load plugin ${ plugin . name } ` , error )
}
}
}
module . exports = new PluginManager ( )