From 16795dd5cc1c4e7eddd9743262c48763c0e12b74 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sat, 16 Dec 2023 22:21:40 +0200 Subject: [PATCH] Add server plugin loader --- .eslintrc.js | 2 +- .gitignore | 1 + default/config.yaml | 2 + plugins/.gitkeep | 0 server.js | 16 ++++- src/plugin-loader.js | 161 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 plugins/.gitkeep create mode 100644 src/plugin-loader.js diff --git a/.eslintrc.js b/.eslintrc.js index 6aef1168..6e926dee 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,7 @@ module.exports = { overrides: [ { // Server-side files (plus this configuration file) - files: ['src/**/*.js', './*.js'], + files: ['src/**/*.js', './*.js', 'plugins/**/*.js'], env: { node: true, }, diff --git a/.gitignore b/.gitignore index de17c931..d82301d8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ access.log /vectors/ /cache/ public/css/user.css +/plugins/ diff --git a/default/config.yaml b/default/config.yaml index b6e52f83..fd0be655 100644 --- a/default/config.yaml +++ b/default/config.yaml @@ -58,3 +58,5 @@ openai: deepl: # Available options: default, more, less, prefer_more, prefer_less formality: default +# -- SERVER PLUGIN CONFIGURATION -- +enableServerPlugins: false diff --git a/plugins/.gitkeep b/plugins/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/server.js b/server.js index 090dcdac..153cb908 100644 --- a/server.js +++ b/server.js @@ -93,9 +93,9 @@ const cliArguments = yargs(hideBin(process.argv)) }).parseSync(); // change all relative paths -const directory = process['pkg'] ? path.dirname(process.execPath) : __dirname; +const serverDirectory = process['pkg'] ? path.dirname(process.execPath) : __dirname; console.log(process['pkg'] ? 'Running from binary' : 'Running from source'); -process.chdir(directory); +process.chdir(serverDirectory); const app = express(); app.use(compression()); @@ -621,6 +621,8 @@ const setupTasks = async function () { exitProcess(); }); + await loadPlugins(); + console.log('Launching...'); if (autorun) open(autorunUrl.toString()); @@ -632,6 +634,16 @@ const setupTasks = async function () { } }; +async function loadPlugins() { + try { + const pluginDirectory = path.join(serverDirectory, 'plugins'); + const loader = require('./src/plugin-loader'); + await loader.loadPlugins(app, pluginDirectory); + } catch { + console.log('Plugin loading failed.'); + } +} + if (listen && !getConfigValue('whitelistMode', true) && !getConfigValue('basicAuthMode', false)) { if (getConfigValue('securityOverride', false)) { console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.')); diff --git a/src/plugin-loader.js b/src/plugin-loader.js new file mode 100644 index 00000000..a644e5e3 --- /dev/null +++ b/src/plugin-loader.js @@ -0,0 +1,161 @@ +const fs = require('fs'); +const path = require('path'); +const { getConfigValue } = require('./util'); +const enableServerPlugins = getConfigValue('enableServerPlugins', false); + +/** + * Determine if a file is a CommonJS module. + * @param {string} file Path to file + * @returns {boolean} True if file is a CommonJS module + */ +const isCommonJS = (file) => path.extname(file) === '.js'; + +/** + * Determine if a file is an ECMAScript module. + * @param {string} file Path to file + * @returns {boolean} True if file is an ECMAScript module + */ +const isESModule = (file) => path.extname(file) === '.mjs'; + +/** + * Load and initialize server plugins from a directory if they are enabled. + * @param {import('express').Express} app Express app + * @param {string} pluginsPath Path to plugins directory + * @returns {Promise} Promise that resolves when all plugins are loaded + */ +async function loadPlugins(app, pluginsPath) { + // Server plugins are disabled. + if (!enableServerPlugins) { + return; + } + + // Plugins directory does not exist. + if (!fs.existsSync(pluginsPath)) { + return; + } + + const files = fs.readdirSync(pluginsPath); + + // No plugins to load. + if (files.length === 0) { + return; + } + + for (const file of files) { + const pluginFilePath = path.join(pluginsPath, file); + + if (fs.statSync(pluginFilePath).isDirectory()) { + await loadFromDirectory(app, pluginFilePath); + } + + // Not a JavaScript file. + if (!isCommonJS(file) && !isESModule(file)) { + continue; + } + + await loadFromFile(app, pluginFilePath); + } +} + +async function loadFromDirectory(app, pluginDirectoryPath) { + const files = fs.readdirSync(pluginDirectoryPath); + + // No plugins to load. + if (files.length === 0) { + return; + } + + // Plugin is an npm package. + const packageJsonFilePath = path.join(pluginDirectoryPath, 'package.json'); + if (fs.existsSync(packageJsonFilePath)) { + if (await loadFromPackage(app, packageJsonFilePath)) { + return; + } + } + + // Plugin is a CommonJS module. + const cjsFilePath = path.join(pluginDirectoryPath, 'index.js'); + if (fs.existsSync(cjsFilePath)) { + if (await loadFromFile(app, cjsFilePath)) { + return; + } + } + + // Plugin is an ECMAScript module. + const esmFilePath = path.join(pluginDirectoryPath, 'index.mjs'); + if (fs.existsSync(esmFilePath)) { + if (await loadFromFile(app, esmFilePath)) { + return; + } + } +} + +/** + * Loads and initializes a plugin from an npm package. + * @param {import('express').Express} app Express app + * @param {string} packageJsonPath Path to package.json file + * @returns {Promise} Promise that resolves to true if plugin was loaded successfully + */ +async function loadFromPackage(app, packageJsonPath) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.main) { + const pluginFilePath = path.join(path.dirname(packageJsonPath), packageJson.main); + return await loadFromFile(app, pluginFilePath); + } + } catch (error) { + console.error(`Failed to load plugin from ${packageJsonPath}: ${error}`); + } + return false; +} + +/** + * Loads and initializes a plugin from a file. + * @param {import('express').Express} app Express app + * @param {string} pluginFilePath Path to plugin directory + * @returns {Promise} Promise that resolves to true if plugin was loaded successfully + */ +async function loadFromFile(app, pluginFilePath) { + try { + const plugin = await getPluginModule(pluginFilePath); + console.log(`Initializing plugin from ${pluginFilePath}`); + return await initPlugin(app, plugin); + } catch (error) { + console.error(`Failed to load plugin from ${pluginFilePath}: ${error}`); + return false; + } +} + +/** + * Initializes a plugin module. + * @param {import('express').Express} app Express app + * @param {any} plugin Plugin module + * @returns {Promise} Promise that resolves to true if plugin was initialized successfully + */ +async function initPlugin(app, plugin) { + if (typeof plugin.init === 'function') { + await plugin.init(app); + return true; + } + + return false; +} + +/** + * Loads a module from a file depending on the module type. + * @param {string} pluginFilePath Path to plugin file + * @returns {Promise} Promise that resolves to plugin module + */ +async function getPluginModule(pluginFilePath) { + if (isCommonJS(pluginFilePath)) { + return require(pluginFilePath); + } + if (isESModule(pluginFilePath)) { + return await import(pluginFilePath); + } + throw new Error(`Unsupported module type in ${pluginFilePath}`); +} + +module.exports = { + loadPlugins, +};