Updates on plugin apis and example

This commit is contained in:
advplyr 2024-12-21 16:48:56 -06:00
parent fc17a74865
commit 048790b33a
11 changed files with 258 additions and 124 deletions

View file

@ -16,7 +16,7 @@ const Logger = require('./Logger')
*/
class Auth {
constructor() {
this.pluginData = []
this.pluginManifests = []
// Map of openId sessions indexed by oauth2 state-variable
this.openIdAuthSession = new Map()
@ -940,7 +940,7 @@ class Auth {
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
serverSettings: Database.serverSettings.toJSONForBrowser(),
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
plugins: this.pluginData,
plugins: this.pluginManifests,
Source: global.Source
}
}

View file

@ -156,7 +156,7 @@ class Server {
// Initialize plugins
await PluginManager.init()
// TODO: Prevents circular dependency for SocketAuthority
this.auth.pluginData = PluginManager.pluginData
this.auth.pluginManifests = PluginManager.pluginManifests
}
/**

View file

@ -1,38 +1,82 @@
const { Request, Response } = require('express')
const { Request, Response, NextFunction } = require('express')
const PluginManager = require('../managers/PluginManager')
const Logger = require('../Logger')
class PluginController {
constructor() {}
/**
*
* @param {Request} req
* @param {Response} res
*/
getConfig(req, res) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
res.json({
config: req.pluginData.instance.config
})
}
/**
* POST: /api/plugins/:id/action
*
* @param {Request} req
* @param {Response} res
*/
handleAction(req, res) {
const pluginId = req.params.id
async handleAction(req, res) {
const actionName = req.body.pluginAction
const target = req.body.target
const data = req.body.data
Logger.info(`[PluginController] Handle plugin action ${pluginId} ${actionName} ${target}`, data)
PluginManager.onAction(pluginId, actionName, target, data)
Logger.info(`[PluginController] Handle plugin "${req.pluginData.manifest.name}" action ${actionName} ${target}`, data)
const actionData = await PluginManager.onAction(req.pluginData, actionName, target, data)
if (!actionData || actionData.error) {
return res.status(400).send(actionData?.error || 'Error performing action')
}
res.sendStatus(200)
}
/**
* POST: /api/plugins/:id/config
*
* @param {*} req
* @param {*} res
* @param {Request} req
* @param {Response} res
*/
handleConfigSave(req, res) {
const pluginId = req.params.id
async handleConfigSave(req, res) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(403)
}
if (!req.body.config || typeof req.body.config !== 'object') {
return res.status(400).send('Invalid config')
}
const config = req.body.config
Logger.info(`[PluginController] Saving config for plugin ${pluginId}`, config)
PluginManager.onConfigSave(pluginId, config)
Logger.info(`[PluginController] Handle save config for plugin ${req.pluginData.manifest.name}`, config)
const saveData = await PluginManager.onConfigSave(req.pluginData, config)
if (!saveData || saveData.error) {
return res.status(400).send(saveData?.error || 'Error saving config')
}
res.sendStatus(200)
}
/**
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async middleware(req, res, next) {
if (req.params.id) {
const pluginData = PluginManager.getPluginDataById(req.params.id)
if (!pluginData) {
return res.sendStatus(404)
}
await pluginData.instance.reload()
req.pluginData = pluginData
}
next()
}
}
module.exports = new PluginController()

View file

@ -12,10 +12,22 @@ const { isUUID, parseSemverStrict } = require('../utils')
* @property {import('../Database')} Database
* @property {import('../SocketAuthority')} SocketAuthority
* @property {import('../managers/TaskManager')} TaskManager
* @property {import('../models/Plugin')} pluginInstance
*/
/**
* @typedef PluginData
* @property {string} id
* @property {Object} manifest
* @property {import('../models/Plugin')} instance
* @property {Function} init
* @property {Function} onAction
* @property {Function} onConfigSave
*/
class PluginManager {
constructor() {
/** @type {PluginData[]} */
this.plugins = []
}
@ -23,29 +35,41 @@ class PluginManager {
return Path.posix.join(global.MetadataPath, 'plugins')
}
get pluginData() {
get pluginManifests() {
return this.plugins.map((plugin) => plugin.manifest)
}
/**
*
* @param {import('../models/Plugin')} pluginInstance
* @returns {PluginContext}
*/
get pluginContext() {
getPluginContext(pluginInstance) {
return {
Logger,
Database,
SocketAuthority,
TaskManager
TaskManager,
pluginInstance
}
}
/**
*
* @param {string} id
* @returns {PluginData}
*/
getPluginDataById(id) {
return this.plugins.find((plugin) => plugin.manifest.id === id)
}
/**
* Validate and load a plugin from a directory
* TODO: Validatation
*
* @param {string} dirname
* @param {string} pluginPath
* @returns {Promise<{manifest: Object, contents: any}>}
* @returns {Promise<PluginData>}
*/
async loadPlugin(dirname, pluginPath) {
const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
@ -88,26 +112,25 @@ class PluginManager {
return null
}
let pluginInstance = null
let pluginContents = null
try {
pluginInstance = require(Path.join(pluginPath, indexFile.name))
pluginContents = require(Path.join(pluginPath, indexFile.name))
} catch (error) {
Logger.error(`Error loading plugin ${pluginPath}`, error)
return null
}
if (typeof pluginInstance.init !== 'function') {
if (typeof pluginContents.init !== 'function') {
Logger.error(`Plugin ${pluginPath} does not have an init function`)
return null
}
return {
id: manifestJson.id,
manifest: manifestJson,
instance: {
init: pluginInstance.init,
onAction: pluginInstance.onAction,
onConfigSave: pluginInstance.onConfigSave
}
init: pluginContents.init,
onAction: pluginContents.onAction,
onConfigSave: pluginContents.onConfigSave
}
}
@ -154,8 +177,9 @@ class PluginManager {
} else {
Logger.debug(`[PluginManager] Plugin "${plugin.manifest.name}" already exists in the database with version "${plugin.manifest.version}"`)
}
plugin.instance = existingPlugin
} else {
await Database.pluginModel.create({
plugin.instance = await Database.pluginModel.create({
id: plugin.manifest.id,
name: plugin.manifest.name,
version: plugin.manifest.version
@ -183,81 +207,49 @@ class PluginManager {
await this.loadPlugins()
for (const plugin of this.plugins) {
if (plugin.instance.init) {
Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`)
plugin.instance.init(this.pluginContext)
}
Logger.info(`[PluginManager] Initializing plugin ${plugin.manifest.name}`)
plugin.init(this.getPluginContext(plugin.instance))
}
}
/**
*
* @param {string} pluginId
* @param {PluginData} plugin
* @param {string} actionName
* @param {string} target
* @param {Object} data
* @returns
* @returns {Promise<boolean|{error:string}>}
*/
onAction(pluginId, actionName, target, data) {
const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId)
if (!plugin) {
Logger.error(`[PluginManager] Plugin ${pluginId} not found`)
return
onAction(plugin, actionName, target, data) {
if (!plugin.onAction) {
Logger.error(`[PluginManager] onAction not implemented for plugin ${plugin.manifest.name}`)
return false
}
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
return false
}
if (plugin.instance.onAction) {
Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`)
plugin.instance.onAction(this.pluginContext, actionName, target, data)
}
Logger.info(`[PluginManager] Calling onAction for plugin ${plugin.manifest.name}`)
return plugin.onAction(this.getPluginContext(plugin.instance), actionName, target, data)
}
/**
*
* @param {string} pluginId
* @param {PluginData} plugin
* @param {Object} config
* @returns {Promise<boolean|{error:string}>}
*/
onConfigSave(pluginId, config) {
const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId)
if (!plugin) {
Logger.error(`[PluginManager] Plugin ${pluginId} not found`)
return
onConfigSave(plugin, config) {
if (!plugin.onConfigSave) {
Logger.error(`[PluginManager] onConfigSave not implemented for plugin ${plugin.manifest.name}`)
return false
}
if (plugin.instance.onConfigSave) {
Logger.info(`[PluginManager] Calling onConfigSave for plugin ${plugin.manifest.name}`)
plugin.instance.onConfigSave(this.pluginContext, config)
}
}
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
const pluginPath = Path.join(this.pluginMetadataPath, plugin.name)
const packageContents = require(pluginPath)
console.log('packageContents', packageContents)
packageContents.init()
this.plugins.push(packageContents)
} catch (error) {
console.log(`Cannot load plugin ${plugin.name}`, error)
}
Logger.info(`[PluginManager] Calling onConfigSave for plugin ${plugin.manifest.name}`)
return plugin.onConfigSave(this.getPluginContext(plugin.instance), config)
}
}
module.exports = new PluginManager()

View file

@ -324,8 +324,9 @@ class ApiRouter {
//
// Plugin routes
//
this.router.post('/plugins/:id/action', PluginController.handleAction.bind(this))
this.router.post('/plugins/:id/config', PluginController.handleConfigSave.bind(this))
this.router.get('/plugins/:id/config', PluginController.middleware.bind(this), PluginController.getConfig.bind(this))
this.router.post('/plugins/:id/action', PluginController.middleware.bind(this), PluginController.handleAction.bind(this))
this.router.post('/plugins/:id/config', PluginController.middleware.bind(this), PluginController.handleConfigSave.bind(this))
//
// Misc Routes