diff --git a/client/pages/config/plugins/_slug.vue b/client/pages/config/plugins/_id.vue similarity index 95% rename from client/pages/config/plugins/_slug.vue rename to client/pages/config/plugins/_id.vue index 619b6608..3f7d84b7 100644 --- a/client/pages/config/plugins/_slug.vue +++ b/client/pages/config/plugins/_id.vue @@ -42,7 +42,7 @@ export default { if (!store.getters['user/getIsAdminOrUp']) { redirect('/') } - const plugin = store.state.plugins.find((plugin) => plugin.slug === params.slug) + const plugin = store.state.plugins.find((plugin) => plugin.id === params.id) if (!plugin) { redirect('/config/plugins') } @@ -95,14 +95,13 @@ export default { console.log('Form data', formData) const payload = { - pluginSlug: this.plugin.slug, config: formData } this.processing = true this.$axios - .$post(`/api/plugins/config`, payload) + .$post(`/api/plugins/${this.plugin.id}/config`, payload) .then(() => { console.log('Plugin configuration saved') }) diff --git a/client/pages/config/plugins/index.vue b/client/pages/config/plugins/index.vue index d9244744..925248f9 100644 --- a/client/pages/config/plugins/index.vue +++ b/client/pages/config/plugins/index.vue @@ -11,7 +11,7 @@
{{ plugin.name }}
{{ plugin.description }}
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index c540fec7..b674e03f 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -434,11 +434,10 @@ export default { if (this.pluginExtensions.length) { this.pluginExtensions.forEach((plugin) => { - const pluginSlug = plugin.slug plugin.extensions.forEach((pext) => { items.push({ text: pext.label, - action: `plugin-${pluginSlug}-action-${pext.name}` + action: `plugin-${plugin.id}-action-${pext.name}` }) }) }) @@ -780,16 +779,15 @@ export default { this.$store.commit('globals/setShareModal', this.mediaItemShare) } else if (action.startsWith('plugin-')) { const actionStrSplit = action.replace('plugin-', '').split('-action-') - const pluginSlug = actionStrSplit[0] + const pluginId = actionStrSplit[0] const pluginAction = actionStrSplit[1] - console.log('Plugin action for', pluginSlug, 'with action', pluginAction) - this.onPluginAction(pluginSlug, pluginAction) + console.log('Plugin action for', pluginId, 'with action', pluginAction) + this.onPluginAction(pluginId, pluginAction) } }, - onPluginAction(pluginSlug, pluginAction) { + onPluginAction(pluginId, pluginAction) { this.$axios - .$post(`/api/plugins/action`, { - pluginSlug, + .$post(`/api/plugins/${pluginId}/action`, { pluginAction, target: 'item.detail.actions', data: { diff --git a/server/Auth.js b/server/Auth.js index f3d2c38d..3557b9b8 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -10,13 +10,14 @@ const ExtractJwt = require('passport-jwt').ExtractJwt const OpenIDClient = require('openid-client') const Database = require('./Database') const Logger = require('./Logger') -const PluginManager = require('./managers/PluginManager') /** * @class Class for handling all the authentication related functionality. */ class Auth { constructor() { + this.pluginData = [] + // Map of openId sessions indexed by oauth2 state-variable this.openIdAuthSession = new Map() this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/] @@ -939,7 +940,7 @@ class Auth { userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), serverSettings: Database.serverSettings.toJSONForBrowser(), ereaderDevices: Database.emailSettings.getEReaderDevices(user), - plugins: PluginManager.pluginData, + plugins: this.pluginData, Source: global.Source } } diff --git a/server/PluginAbstract.js b/server/PluginAbstract.js deleted file mode 100644 index d5d56eff..00000000 --- a/server/PluginAbstract.js +++ /dev/null @@ -1,20 +0,0 @@ -class PluginAbstract { - constructor() { - if (this.constructor === PluginAbstract) { - throw new Error('Cannot instantiate abstract class') - } - } - - init() { - throw new Error('Method "init()" not implemented') - } - - onAction() { - throw new Error('Method "onAction()" not implemented') - } - - onDestroy() { - throw new Error('Method "onDestroy()" not implemented') - } -} -module.exports = PluginAbstract diff --git a/server/Server.js b/server/Server.js index a707958a..077e8257 100644 --- a/server/Server.js +++ b/server/Server.js @@ -154,7 +154,9 @@ class Server { } // Initialize plugins - PluginManager.init() + await PluginManager.init() + // TODO: Prevents circular dependency for SocketAuthority + this.auth.pluginData = PluginManager.pluginData } /** diff --git a/server/controllers/PluginController.js b/server/controllers/PluginController.js index c69ecc0c..ae983fac 100644 --- a/server/controllers/PluginController.js +++ b/server/controllers/PluginController.js @@ -6,32 +6,32 @@ class PluginController { constructor() {} /** - * POST: /api/plugins/action + * POST: /api/plugins/:id/action * * @param {Request} req * @param {Response} res */ handleAction(req, res) { - const pluginSlug = req.body.pluginSlug + const pluginId = req.params.id const actionName = req.body.pluginAction const target = req.body.target const data = req.body.data - Logger.info(`[PluginController] Handle plugin action ${pluginSlug} ${actionName} ${target}`, data) - PluginManager.onAction(pluginSlug, actionName, target, data) + Logger.info(`[PluginController] Handle plugin action ${pluginId} ${actionName} ${target}`, data) + PluginManager.onAction(pluginId, actionName, target, data) res.sendStatus(200) } /** - * POST: /api/plugins/config + * POST: /api/plugins/:id/config * * @param {*} req * @param {*} res */ handleConfigSave(req, res) { - const pluginSlug = req.body.pluginSlug + const pluginId = req.params.id const config = req.body.config - Logger.info(`[PluginController] Saving config for plugin ${pluginSlug}`, config) - PluginManager.onConfigSave(pluginSlug, config) + Logger.info(`[PluginController] Saving config for plugin ${pluginId}`, config) + PluginManager.onConfigSave(pluginId, config) res.sendStatus(200) } } diff --git a/server/managers/PluginManager.js b/server/managers/PluginManager.js index 9b3ba8f3..a42eb8d7 100644 --- a/server/managers/PluginManager.js +++ b/server/managers/PluginManager.js @@ -1,14 +1,17 @@ const Path = require('path') const Logger = require('../Logger') const Database = require('../Database') -const PluginAbstract = require('../PluginAbstract') +const SocketAuthority = require('../SocketAuthority') +const TaskManager = require('../managers/TaskManager') const fsExtra = require('../libs/fsExtra') const { isUUID, parseSemverStrict } = require('../utils') /** * @typedef PluginContext - * @property {import('../../server/Logger')} Logger - * @property {import('../../server/Database')} Database + * @property {import('../Logger')} Logger + * @property {import('../Database')} Database + * @property {import('../SocketAuthority')} SocketAuthority + * @property {import('../managers/TaskManager')} TaskManager */ class PluginManager { @@ -30,7 +33,9 @@ class PluginManager { get pluginContext() { return { Logger, - Database + Database, + SocketAuthority, + TaskManager } } @@ -40,7 +45,7 @@ class PluginManager { * * @param {string} dirname * @param {string} pluginPath - * @returns {Promise<{manifest: Object, contents: PluginAbstract}>} + * @returns {Promise<{manifest: Object, contents: any}>} */ async loadPlugin(dirname, pluginPath) { const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory())) @@ -98,7 +103,11 @@ class PluginManager { return { manifest: manifestJson, - instance: pluginInstance + instance: { + init: pluginInstance.init, + onAction: pluginInstance.onAction, + onConfigSave: pluginInstance.onConfigSave + } } } @@ -181,10 +190,18 @@ class PluginManager { } } - onAction(pluginSlug, actionName, target, data) { - const plugin = this.plugins.find((plugin) => plugin.manifest.slug === pluginSlug) + /** + * + * @param {string} pluginId + * @param {string} actionName + * @param {string} target + * @param {Object} data + * @returns + */ + onAction(pluginId, actionName, target, data) { + const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId) if (!plugin) { - Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`) + Logger.error(`[PluginManager] Plugin ${pluginId} not found`) return } @@ -200,10 +217,15 @@ class PluginManager { } } - onConfigSave(pluginSlug, config) { - const plugin = this.plugins.find((plugin) => plugin.manifest.slug === pluginSlug) + /** + * + * @param {string} pluginId + * @param {Object} config + */ + onConfigSave(pluginId, config) { + const plugin = this.plugins.find((plugin) => plugin.manifest.id === pluginId) if (!plugin) { - Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`) + Logger.error(`[PluginManager] Plugin ${pluginId} not found`) return } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index beb3db1f..32c650a9 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -324,8 +324,8 @@ class ApiRouter { // // Plugin routes // - this.router.post('/plugins/action', PluginController.handleAction.bind(this)) - this.router.post('/plugins/config', PluginController.handleConfigSave.bind(this)) + this.router.post('/plugins/:id/action', PluginController.handleAction.bind(this)) + this.router.post('/plugins/:id/config', PluginController.handleConfigSave.bind(this)) // // Misc Routes diff --git a/test/server/managers/plugins/Example/index.js b/test/server/managers/plugins/Example/index.js index 6f3e5603..3636288d 100644 --- a/test/server/managers/plugins/Example/index.js +++ b/test/server/managers/plugins/Example/index.js @@ -1,54 +1,84 @@ -class ExamplePlugin { - constructor() { - this.name = 'Example' - } +/** + * Called on initialization of the plugin + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + */ +module.exports.init = async (context) => { + context.Logger.info('[ExamplePlugin] Example plugin initialized') - /** - * - * @param {import('../../server/managers/PluginManager').PluginContext} context - */ - async init(context) { - context.Logger.info('[ExamplePlugin] Example plugin loaded successfully') + context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => { + context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`) + handleMediaProgressUpdate(context, instance) + }) - context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => { - context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`) - this.handleMediaProgressUpdate(context, instance) - }) - } + sendAdminMessageToast(context) +} - /** - * @param {import('../../server/managers/PluginManager').PluginContext} context - * @param {import('../../server/models/MediaProgress')} mediaProgress - */ - async handleMediaProgressUpdate(context, mediaProgress) { - const mediaItem = await mediaProgress.getMediaItem() - if (!mediaItem) { - context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`) - } else { - const mediaProgressDuration = mediaProgress.duration - const progressPercent = mediaProgressDuration > 0 ? (mediaProgress.currentTime / mediaProgressDuration) * 100 : 0 - context.Logger.info(`[ExamplePlugin] Media progress update for "${mediaItem.title}" ${Math.round(progressPercent)}%`) - } - } +/** + * Called when an extension action is triggered + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + * @param {string} actionName + * @param {string} target + * @param {*} data + */ +module.exports.onAction = async (context, actionName, target, data) => { + context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data) +} - /** - * - * @param {import('../../server/managers/PluginManager').PluginContext} context - * @param {string} actionName - * @param {string} target - * @param {*} data - */ - async onAction(context, actionName, target, data) { - context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data) - } +/** + * Called when the plugin config page is saved + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + * @param {*} config + */ +module.exports.onConfigSave = async (context, config) => { + context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config) - /** - * - * @param {import('../../server/managers/PluginManager').PluginContext} context - * @param {*} config - */ - async onConfigSave(context, config) { - context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config) + createTask(context) +} + +// +// Helper functions +// + +/** + * Scrobble media progress update + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + * @param {import('../../../server/models/MediaProgress')} mediaProgress + */ +async function handleMediaProgressUpdate(context, mediaProgress) { + const mediaItem = await mediaProgress.getMediaItem() + if (!mediaItem) { + context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`) + } else { + const mediaProgressDuration = mediaProgress.duration + const progressPercent = mediaProgressDuration > 0 ? (mediaProgress.currentTime / mediaProgressDuration) * 100 : 0 + context.Logger.info(`[ExamplePlugin] Media progress update for "${mediaItem.title}" ${Math.round(progressPercent)}%`) } } -module.exports = new ExamplePlugin() + +/** + * Test socket authority + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + */ +async function sendAdminMessageToast(context) { + setTimeout(() => { + context.SocketAuthority.adminEmitter('admin_message', 'Hello from ExamplePlugin!') + }, 10000) +} + +/** + * Test task manager + * + * @param {import('../../../server/managers/PluginManager').PluginContext} context + */ +async function createTask(context) { + const task = context.TaskManager.createAndAddTask('example_action', { text: 'Example Task' }, { text: 'This is an example task' }, true) + setTimeout(() => { + task.setFinished({ text: 'Example Task Finished' }) + context.TaskManager.taskFinished(task) + }, 5000) +} diff --git a/test/server/managers/plugins/Example/manifest.json b/test/server/managers/plugins/Example/manifest.json index 33b25d24..f0f97075 100644 --- a/test/server/managers/plugins/Example/manifest.json +++ b/test/server/managers/plugins/Example/manifest.json @@ -1,5 +1,6 @@ { - "name": "Example Plugin", + "id": "e6205690-916c-4add-9a2b-2548266996ef", + "name": "Example", "slug": "example", "version": "1.0.0", "description": "This is an example plugin",