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-21 21:54:43 +01:00
const SocketAuthority = require ( '../SocketAuthority' )
const TaskManager = require ( '../managers/TaskManager' )
2024-12-22 22:15:56 +01:00
const ShareManager = require ( '../managers/ShareManager' )
const RssFeedManager = require ( '../managers/RssFeedManager' )
const PodcastManager = require ( '../managers/PodcastManager' )
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
2024-12-21 21:54:43 +01:00
* @ property { import ( '../Logger' ) } Logger
* @ property { import ( '../Database' ) } Database
* @ property { import ( '../SocketAuthority' ) } SocketAuthority
* @ property { import ( '../managers/TaskManager' ) } TaskManager
2024-12-21 23:48:56 +01:00
* @ property { import ( '../models/Plugin' ) } pluginInstance
2024-12-22 22:15:56 +01:00
* @ property { import ( '../managers/ShareManager' ) } ShareManager
* @ property { import ( '../managers/RssFeedManager' ) } RssFeedManager
* @ property { import ( '../managers/PodcastManager' ) } PodcastManager
2024-12-21 23:48:56 +01:00
* /
/ * *
* @ typedef PluginData
* @ property { string } id
* @ property { Object } manifest
* @ property { import ( '../models/Plugin' ) } instance
* @ property { Function } init
* @ property { Function } onAction
* @ property { Function } onConfigSave
2024-12-21 00:21:00 +01:00
* /
2024-12-20 00:48:18 +01:00
class PluginManager {
constructor ( ) {
2024-12-21 23:48:56 +01:00
/** @type {PluginData[]} */
2024-12-20 00:48:18 +01:00
this . plugins = [ ]
}
2024-12-21 17:20:09 +01:00
get pluginMetadataPath ( ) {
return Path . posix . join ( global . MetadataPath , 'plugins' )
}
2024-12-21 23:48:56 +01:00
get pluginManifests ( ) {
2024-12-21 00:21:00 +01:00
return this . plugins . map ( ( plugin ) => plugin . manifest )
2024-12-20 00:48:18 +01:00
}
2024-12-21 00:21:00 +01:00
/ * *
2024-12-21 23:48:56 +01:00
*
* @ param { import ( '../models/Plugin' ) } pluginInstance
2024-12-21 00:21:00 +01:00
* @ returns { PluginContext }
* /
2024-12-21 23:48:56 +01:00
getPluginContext ( pluginInstance ) {
2024-12-20 00:48:18 +01:00
return {
2024-12-21 00:21:00 +01:00
Logger ,
2024-12-21 21:54:43 +01:00
Database ,
SocketAuthority ,
2024-12-21 23:48:56 +01:00
TaskManager ,
2024-12-22 22:15:56 +01:00
pluginInstance ,
ShareManager ,
RssFeedManager ,
PodcastManager
2024-12-20 00:48:18 +01:00
}
}
2024-12-21 23:48:56 +01:00
/ * *
*
* @ param { string } id
* @ returns { PluginData }
* /
getPluginDataById ( id ) {
return this . plugins . find ( ( plugin ) => plugin . manifest . id === id )
}
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
2024-12-21 23:48:56 +01:00
* @ returns { Promise < PluginData > }
2024-12-20 00:48:18 +01:00
* /
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 23:48:56 +01:00
let pluginContents = null
2024-12-20 00:48:18 +01:00
try {
2024-12-21 23:48:56 +01:00
pluginContents = 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 23:48:56 +01:00
if ( typeof pluginContents . init !== 'function' ) {
2024-12-21 00:21:00 +01:00
Logger . error ( ` Plugin ${ pluginPath } does not have an init function ` )
return null
}
2024-12-20 00:48:18 +01:00
return {
2024-12-21 23:48:56 +01:00
id : manifestJson . id ,
2024-12-20 00:48:18 +01:00
manifest : manifestJson ,
2024-12-21 23:48:56 +01:00
init : pluginContents . init ,
onAction : pluginContents . onAction ,
onConfigSave : pluginContents . onConfigSave
2024-12-20 00:48:18 +01:00
}
}
2024-12-21 20:26:42 +01:00
/ * *
* Get all plugins from the / metadata / plugins directory
* /
2024-12-24 20:04:54 +01:00
async getPluginsFromDirPath ( pluginsPath ) {
2024-12-21 20:26:42 +01:00
// Get all directories in the plugins directory
2024-12-24 20:04:54 +01:00
const pluginDirs = await fsExtra . readdir ( pluginsPath , { withFileTypes : 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 ` )
2024-12-24 20:04:54 +01:00
const plugin = await this . loadPlugin ( pluginDir . name , Path . join ( pluginsPath , 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 )
}
}
2024-12-24 20:04:54 +01:00
2024-12-21 20:26:42 +01:00
return pluginsFound
}
/ * *
* Load plugins from the / metadata / plugins directory and update the database
* /
async loadPlugins ( ) {
2024-12-24 20:04:54 +01:00
await fsExtra . ensureDir ( this . pluginMetadataPath )
const pluginsFound = await this . getPluginsFromDirPath ( this . pluginMetadataPath )
if ( process . env . DEV _PLUGINS _PATH ) {
const devPluginsFound = await this . getPluginsFromDirPath ( process . env . DEV _PLUGINS _PATH )
if ( ! devPluginsFound . length ) {
Logger . warn ( ` [PluginManager] No plugins found in DEV_PLUGINS_PATH: ${ process . env . DEV _PLUGINS _PATH } ` )
} else {
pluginsFound . push ( ... devPluginsFound )
}
}
2024-12-21 20:26:42 +01:00
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 } " ` )
}
2024-12-21 23:48:56 +01:00
plugin . instance = existingPlugin
2024-12-21 20:26:42 +01:00
} else {
2024-12-21 23:48:56 +01:00
plugin . instance = await Database . pluginModel . create ( {
2024-12-21 20:26:42 +01:00
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 23:48:56 +01:00
Logger . info ( ` [PluginManager] Initializing plugin ${ plugin . manifest . name } ` )
plugin . init ( this . getPluginContext ( plugin . instance ) )
2024-12-20 00:48:18 +01:00
}
}
2024-12-21 21:54:43 +01:00
/ * *
*
2024-12-21 23:48:56 +01:00
* @ param { PluginData } plugin
2024-12-21 21:54:43 +01:00
* @ param { string } actionName
* @ param { string } target
* @ param { Object } data
2024-12-21 23:48:56 +01:00
* @ returns { Promise < boolean | { error : string } > }
2024-12-21 21:54:43 +01:00
* /
2024-12-21 23:48:56 +01:00
onAction ( plugin , actionName , target , data ) {
if ( ! plugin . onAction ) {
Logger . error ( ` [PluginManager] onAction not implemented for plugin ${ plugin . manifest . name } ` )
return false
2024-12-20 00:48:18 +01:00
}
const pluginExtension = plugin . manifest . extensions . find ( ( extension ) => extension . name === actionName )
if ( ! pluginExtension ) {
Logger . error ( ` [PluginManager] Extension ${ actionName } not found for plugin ${ plugin . manifest . name } ` )
2024-12-21 23:48:56 +01:00
return false
2024-12-20 00:48:18 +01:00
}
2024-12-21 23:48:56 +01:00
Logger . info ( ` [PluginManager] Calling onAction for plugin ${ plugin . manifest . name } ` )
return plugin . onAction ( this . getPluginContext ( plugin . instance ) , actionName , target , data )
2024-12-21 00:21:00 +01:00
}
2024-12-21 21:54:43 +01:00
/ * *
*
2024-12-21 23:48:56 +01:00
* @ param { PluginData } plugin
2024-12-21 21:54:43 +01:00
* @ param { Object } config
2024-12-21 23:48:56 +01:00
* @ returns { Promise < boolean | { error : string } > }
2024-12-21 21:54:43 +01:00
* /
2024-12-21 23:48:56 +01:00
onConfigSave ( plugin , config ) {
if ( ! plugin . onConfigSave ) {
Logger . error ( ` [PluginManager] onConfigSave not implemented for plugin ${ plugin . manifest . name } ` )
return false
2024-12-21 00:21:00 +01:00
}
2024-12-21 23:48:56 +01:00
Logger . info ( ` [PluginManager] Calling onConfigSave for plugin ${ plugin . manifest . name } ` )
return plugin . onConfigSave ( this . getPluginContext ( plugin . instance ) , config )
2024-12-20 00:48:18 +01:00
}
}
module . exports = new PluginManager ( )