Update plugin to use uuid for id, update example plugin with taskmanager and socketauthority test

This commit is contained in:
advplyr 2024-12-21 14:54:43 -06:00
parent cfe3deff3b
commit fc17a74865
11 changed files with 138 additions and 105 deletions

View File

@ -42,7 +42,7 @@ export default {
if (!store.getters['user/getIsAdminOrUp']) { if (!store.getters['user/getIsAdminOrUp']) {
redirect('/') 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) { if (!plugin) {
redirect('/config/plugins') redirect('/config/plugins')
} }
@ -95,14 +95,13 @@ export default {
console.log('Form data', formData) console.log('Form data', formData)
const payload = { const payload = {
pluginSlug: this.plugin.slug,
config: formData config: formData
} }
this.processing = true this.processing = true
this.$axios this.$axios
.$post(`/api/plugins/config`, payload) .$post(`/api/plugins/${this.plugin.id}/config`, payload)
.then(() => { .then(() => {
console.log('Plugin configuration saved') console.log('Plugin configuration saved')
}) })

View File

@ -11,7 +11,7 @@
<h2 class="text-xl font-medium">Installed Plugins</h2> <h2 class="text-xl font-medium">Installed Plugins</h2>
<template v-for="plugin in plugins"> <template v-for="plugin in plugins">
<nuxt-link :key="plugin.slug" :to="`/config/plugins/${plugin.slug}`" class="flex items-center bg-primary rounded-md shadow-sm p-4 my-4 space-x-4"> <nuxt-link :key="plugin.id" :to="`/config/plugins/${plugin.id}`" class="flex items-center bg-primary rounded-md shadow-sm p-4 my-4 space-x-4">
<p class="text-lg">{{ plugin.name }}</p> <p class="text-lg">{{ plugin.name }}</p>
<p class="text-sm text-gray-300">{{ plugin.description }}</p> <p class="text-sm text-gray-300">{{ plugin.description }}</p>
<div class="flex-grow" /> <div class="flex-grow" />

View File

@ -434,11 +434,10 @@ export default {
if (this.pluginExtensions.length) { if (this.pluginExtensions.length) {
this.pluginExtensions.forEach((plugin) => { this.pluginExtensions.forEach((plugin) => {
const pluginSlug = plugin.slug
plugin.extensions.forEach((pext) => { plugin.extensions.forEach((pext) => {
items.push({ items.push({
text: pext.label, 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) this.$store.commit('globals/setShareModal', this.mediaItemShare)
} else if (action.startsWith('plugin-')) { } else if (action.startsWith('plugin-')) {
const actionStrSplit = action.replace('plugin-', '').split('-action-') const actionStrSplit = action.replace('plugin-', '').split('-action-')
const pluginSlug = actionStrSplit[0] const pluginId = actionStrSplit[0]
const pluginAction = actionStrSplit[1] const pluginAction = actionStrSplit[1]
console.log('Plugin action for', pluginSlug, 'with action', pluginAction) console.log('Plugin action for', pluginId, 'with action', pluginAction)
this.onPluginAction(pluginSlug, pluginAction) this.onPluginAction(pluginId, pluginAction)
} }
}, },
onPluginAction(pluginSlug, pluginAction) { onPluginAction(pluginId, pluginAction) {
this.$axios this.$axios
.$post(`/api/plugins/action`, { .$post(`/api/plugins/${pluginId}/action`, {
pluginSlug,
pluginAction, pluginAction,
target: 'item.detail.actions', target: 'item.detail.actions',
data: { data: {

View File

@ -10,13 +10,14 @@ const ExtractJwt = require('passport-jwt').ExtractJwt
const OpenIDClient = require('openid-client') const OpenIDClient = require('openid-client')
const Database = require('./Database') const Database = require('./Database')
const Logger = require('./Logger') const Logger = require('./Logger')
const PluginManager = require('./managers/PluginManager')
/** /**
* @class Class for handling all the authentication related functionality. * @class Class for handling all the authentication related functionality.
*/ */
class Auth { class Auth {
constructor() { constructor() {
this.pluginData = []
// Map of openId sessions indexed by oauth2 state-variable // Map of openId sessions indexed by oauth2 state-variable
this.openIdAuthSession = new Map() this.openIdAuthSession = new Map()
this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/] this.ignorePatterns = [/\/api\/items\/[^/]+\/cover/, /\/api\/authors\/[^/]+\/image/]
@ -939,7 +940,7 @@ class Auth {
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
serverSettings: Database.serverSettings.toJSONForBrowser(), serverSettings: Database.serverSettings.toJSONForBrowser(),
ereaderDevices: Database.emailSettings.getEReaderDevices(user), ereaderDevices: Database.emailSettings.getEReaderDevices(user),
plugins: PluginManager.pluginData, plugins: this.pluginData,
Source: global.Source Source: global.Source
} }
} }

View File

@ -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

View File

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

View File

@ -6,32 +6,32 @@ class PluginController {
constructor() {} constructor() {}
/** /**
* POST: /api/plugins/action * POST: /api/plugins/:id/action
* *
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
handleAction(req, res) { handleAction(req, res) {
const pluginSlug = req.body.pluginSlug const pluginId = req.params.id
const actionName = req.body.pluginAction const actionName = req.body.pluginAction
const target = req.body.target const target = req.body.target
const data = req.body.data const data = req.body.data
Logger.info(`[PluginController] Handle plugin action ${pluginSlug} ${actionName} ${target}`, data) Logger.info(`[PluginController] Handle plugin action ${pluginId} ${actionName} ${target}`, data)
PluginManager.onAction(pluginSlug, actionName, target, data) PluginManager.onAction(pluginId, actionName, target, data)
res.sendStatus(200) res.sendStatus(200)
} }
/** /**
* POST: /api/plugins/config * POST: /api/plugins/:id/config
* *
* @param {*} req * @param {*} req
* @param {*} res * @param {*} res
*/ */
handleConfigSave(req, res) { handleConfigSave(req, res) {
const pluginSlug = req.body.pluginSlug const pluginId = req.params.id
const config = req.body.config const config = req.body.config
Logger.info(`[PluginController] Saving config for plugin ${pluginSlug}`, config) Logger.info(`[PluginController] Saving config for plugin ${pluginId}`, config)
PluginManager.onConfigSave(pluginSlug, config) PluginManager.onConfigSave(pluginId, config)
res.sendStatus(200) res.sendStatus(200)
} }
} }

View File

@ -1,14 +1,17 @@
const Path = require('path') const Path = require('path')
const Logger = require('../Logger') const Logger = require('../Logger')
const Database = require('../Database') const Database = require('../Database')
const PluginAbstract = require('../PluginAbstract') const SocketAuthority = require('../SocketAuthority')
const TaskManager = require('../managers/TaskManager')
const fsExtra = require('../libs/fsExtra') const fsExtra = require('../libs/fsExtra')
const { isUUID, parseSemverStrict } = require('../utils') const { isUUID, parseSemverStrict } = require('../utils')
/** /**
* @typedef PluginContext * @typedef PluginContext
* @property {import('../../server/Logger')} Logger * @property {import('../Logger')} Logger
* @property {import('../../server/Database')} Database * @property {import('../Database')} Database
* @property {import('../SocketAuthority')} SocketAuthority
* @property {import('../managers/TaskManager')} TaskManager
*/ */
class PluginManager { class PluginManager {
@ -30,7 +33,9 @@ class PluginManager {
get pluginContext() { get pluginContext() {
return { return {
Logger, Logger,
Database Database,
SocketAuthority,
TaskManager
} }
} }
@ -40,7 +45,7 @@ class PluginManager {
* *
* @param {string} dirname * @param {string} dirname
* @param {string} pluginPath * @param {string} pluginPath
* @returns {Promise<{manifest: Object, contents: PluginAbstract}>} * @returns {Promise<{manifest: Object, contents: any}>}
*/ */
async loadPlugin(dirname, pluginPath) { async loadPlugin(dirname, pluginPath) {
const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory())) const pluginFiles = await fsExtra.readdir(pluginPath, { withFileTypes: true }).then((files) => files.filter((file) => !file.isDirectory()))
@ -98,7 +103,11 @@ class PluginManager {
return { return {
manifest: manifestJson, 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) { if (!plugin) {
Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`) Logger.error(`[PluginManager] Plugin ${pluginId} not found`)
return 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) { if (!plugin) {
Logger.error(`[PluginManager] Plugin ${pluginSlug} not found`) Logger.error(`[PluginManager] Plugin ${pluginId} not found`)
return return
} }

View File

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

View File

@ -1,26 +1,54 @@
class ExamplePlugin {
constructor() {
this.name = 'Example'
}
/** /**
* Called on initialization of the plugin
* *
* @param {import('../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
*/ */
async init(context) { module.exports.init = async (context) => {
context.Logger.info('[ExamplePlugin] Example plugin loaded successfully') context.Logger.info('[ExamplePlugin] Example plugin initialized')
context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => { context.Database.mediaProgressModel.addHook('afterSave', (instance, options) => {
context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`) context.Logger.debug(`[ExamplePlugin] mediaProgressModel afterSave hook for mediaProgress ${instance.id}`)
this.handleMediaProgressUpdate(context, instance) handleMediaProgressUpdate(context, instance)
}) })
sendAdminMessageToast(context)
} }
/** /**
* @param {import('../../server/managers/PluginManager').PluginContext} context * Called when an extension action is triggered
* @param {import('../../server/models/MediaProgress')} mediaProgress *
* @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {string} actionName
* @param {string} target
* @param {*} data
*/ */
async handleMediaProgressUpdate(context, mediaProgress) { module.exports.onAction = async (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)
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() const mediaItem = await mediaProgress.getMediaItem()
if (!mediaItem) { if (!mediaItem) {
context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`) context.Logger.error(`[ExamplePlugin] Media item not found for mediaProgress ${mediaProgress.id}`)
@ -32,23 +60,25 @@ class ExamplePlugin {
} }
/** /**
* Test socket authority
* *
* @param {import('../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {string} actionName
* @param {string} target
* @param {*} data
*/ */
async onAction(context, actionName, target, data) { async function sendAdminMessageToast(context) {
context.Logger.info('[ExamplePlugin] Example plugin onAction', actionName, target, data) setTimeout(() => {
context.SocketAuthority.adminEmitter('admin_message', 'Hello from ExamplePlugin!')
}, 10000)
} }
/** /**
* Test task manager
* *
* @param {import('../../server/managers/PluginManager').PluginContext} context * @param {import('../../../server/managers/PluginManager').PluginContext} context
* @param {*} config
*/ */
async onConfigSave(context, config) { async function createTask(context) {
context.Logger.info('[ExamplePlugin] Example plugin onConfigSave', config) 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)
} }
}
module.exports = new ExamplePlugin()

View File

@ -1,5 +1,6 @@
{ {
"name": "Example Plugin", "id": "e6205690-916c-4add-9a2b-2548266996ef",
"name": "Example",
"slug": "example", "slug": "example",
"version": "1.0.0", "version": "1.0.0",
"description": "This is an example plugin", "description": "This is an example plugin",