From 779efca39a27b2c756bcf99cc577510d96955151 Mon Sep 17 00:00:00 2001 From: Karanja-Stephen Date: Tue, 10 Feb 2026 13:13:24 +0300 Subject: [PATCH 1/3] feat: improve plugin.json with multiple authors, versions, and pre-compilation dependency checking - Add support for multiple authors and contributors in plugin.json - Maintain backwards compatibility with single author (string) format - Add versions array to specify compatible Open Ticket versions (e.g., OTv4.0.x, ODv1.0.0) - Implement version compatibility checking during plugin loading - Read plugin.json files before TypeScript compilation to check npm dependencies - Warn about missing npm dependencies before compilation starts - Update example plugin to demonstrate new features Fixes #162 --- index.js | 48 +++++++++++++ plugins/example-plugin/plugin.json | 3 + src/core/api/modules/plugin.ts | 32 ++++++++- src/core/startup/pluginLauncher.ts | 111 ++++++++++++++++++++++++++++- 4 files changed, 189 insertions(+), 5 deletions(-) diff --git a/index.js b/index.js index c3bdea0..5c5aa51 100644 --- a/index.js +++ b/index.js @@ -84,6 +84,54 @@ function saveNewCompilationHash(){ } if (!process.argv.includes("--no-compile")){ + // Read plugin.json files before compilation to check for npm dependencies + const pluginDependencies = new Set() + if (fs.existsSync("./plugins")){ + console.log("OT: Reading plugin.json files...") + const plugins = fs.readdirSync("./plugins") + for (const pluginDir of plugins){ + if (pluginDir === ".DS_Store") continue + const pluginPath = nodepath.join("./plugins", pluginDir) + if (!fs.statSync(pluginPath).isDirectory()) continue + + const pluginJsonPath = nodepath.join(pluginPath, "plugin.json") + if (fs.existsSync(pluginJsonPath)){ + try { + const pluginData = JSON.parse(fs.readFileSync(pluginJsonPath).toString()) + if (pluginData.npmDependencies && Array.isArray(pluginData.npmDependencies)){ + pluginData.npmDependencies.forEach(dep => { + if (typeof dep === "string" && dep.trim()) { + pluginDependencies.add(dep.trim()) + } + }) + } + } catch (e) { + // Silently skip invalid plugin.json files - they'll be caught during plugin loading + } + } + } + + // Check for missing dependencies + if (pluginDependencies.size > 0){ + console.log("OT: Checking plugin npm dependencies...") + const missingDeps = [] + for (const dep of pluginDependencies){ + try { + require.resolve(dep) + } catch { + missingDeps.push(dep) + } + } + + if (missingDeps.length > 0){ + console.log("OT: Warning - Missing npm dependencies required by plugins:") + missingDeps.forEach(dep => console.log(` - ${dep}`)) + console.log("OT: Please install missing dependencies with: npm install " + missingDeps.join(" ")) + console.log("OT: Continuing compilation anyway...") + } + } + } + if (requiresCompilation()){ console.log("OT: Compilation Required...") diff --git a/plugins/example-plugin/plugin.json b/plugins/example-plugin/plugin.json index 264b156..c8eae73 100644 --- a/plugins/example-plugin/plugin.json +++ b/plugins/example-plugin/plugin.json @@ -14,6 +14,9 @@ "details":{ "author":"DJj123dj", + "authors":["DJj123dj"], + "contributors":[], + "versions":["OTv4.0.x","OTv4.1.x"], "shortDescription":"A simple template for an Open Ticket v4 plugin!", "longDescription":"A simple example of an Open Ticket v4 plugin!", "imageUrl":"", diff --git a/src/core/api/modules/plugin.ts b/src/core/api/modules/plugin.ts index cf0293a..a2f4ac4 100644 --- a/src/core/api/modules/plugin.ts +++ b/src/core/api/modules/plugin.ts @@ -78,8 +78,14 @@ export interface ODPluginData { * Additional details in the `plugin.json` file from a plugin. */ export interface ODPluginDetails { - /**The author of the plugin. */ - author:string, + /**The author of the plugin. (string for backwards compatibility, or string[] for multiple authors) */ + author:string|string[], + /**A list of authors of the plugin. (new format, optional if author is provided) */ + authors?:string[], + /**A list of contributors to the plugin. (optional) */ + contributors?:string[], + /**A list of compatible versions. (e.g. ["OTv4.0.x", "OTv4.1.x", "ODv1.0.0"]) */ + versions?:string[], /**A short description of this plugin. */ shortDescription:string, /**A large description of this plugin. */ @@ -212,6 +218,28 @@ export class ODPlugin extends ODManagerData { return incompatible } + + /**Get all authors as an array. Handles both old (string) and new (array) format. */ + getAuthors(): string[] { + if (Array.isArray(this.details.author)) { + return this.details.author + } else if (this.details.authors && Array.isArray(this.details.authors)) { + return this.details.authors + } else if (typeof this.details.author === "string") { + return [this.details.author] + } + return [] + } + + /**Get all contributors as an array. */ + getContributors(): string[] { + return this.details.contributors || [] + } + + /**Get all compatible versions as an array. */ + getCompatibleVersions(): string[] { + return this.details.versions || [] + } } /**## ODPluginClassManager `class` diff --git a/src/core/startup/pluginLauncher.ts b/src/core/startup/pluginLauncher.ts index f8e219f..49d2c5e 100644 --- a/src/core/startup/pluginLauncher.ts +++ b/src/core/startup/pluginLauncher.ts @@ -47,7 +47,49 @@ export const loadAllPlugins = async () => { if (!Array.isArray(rawplugindata.incompatiblePlugins)) throw new ODPluginError("Failed to load plugin.json/incompatiblePlugins") if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details") - if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author") + + // Handle author field - support both old (string) and new (array) format for backwards compatibility + if (typeof rawplugindata.details.author != "string" && !Array.isArray(rawplugindata.details.author)) { + throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)") + } + + // Normalize author to array format for internal use + if (typeof rawplugindata.details.author == "string") { + // Old format: convert string to array + rawplugindata.details.authors = [rawplugindata.details.author] + } else if (Array.isArray(rawplugindata.details.author)) { + // New format: use author array as authors + rawplugindata.details.authors = rawplugindata.details.author + } + + // Validate authors array if provided separately + if (rawplugindata.details.authors && !Array.isArray(rawplugindata.details.authors)) { + throw new ODPluginError("Failed to load plugin.json/details/authors (must be array)") + } + + // Validate contributors array if provided + if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) { + throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)") + } + + // Validate versions array if provided + if (rawplugindata.details.versions) { + if (!Array.isArray(rawplugindata.details.versions)) { + throw new ODPluginError("Failed to load plugin.json/details/versions (must be array)") + } + // Validate version format: should match pattern like "OTv4.0.x", "ODv1.0.0", etc. + for (const version of rawplugindata.details.versions) { + if (typeof version != "string") { + throw new ODPluginError("Failed to load plugin.json/details/versions (all items must be strings)") + } + // Check format: project prefix (OT, OD, OM, OU) + v + version pattern + const versionPattern = /^(OT|OD|OM|OU)v\d+\.\d+(\.\d+|\.x)$/ + if (!versionPattern.test(version)) { + throw new ODPluginError(`Failed to load plugin.json/details/versions (invalid format: "${version}", expected format like "OTv4.0.x" or "ODv1.0.0")`) + } + } + } + if (typeof rawplugindata.details.shortDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/shortDescription") if (typeof rawplugindata.details.longDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/longDescription") if (typeof rawplugindata.details.imageUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/imageUrl") @@ -91,6 +133,7 @@ export const loadAllPlugins = async () => { const incompatibilities: {from:string,to:string}[] = [] const missingDependencies: {id:string,missing:string}[] = [] const missingPlugins: {id:string,missing:string}[] = [] + const versionIncompatibilities: {id:string}[] = [] //go through all plugins for errors sortedPlugins.filter((plugin) => plugin.enabled).forEach((plugin) => { @@ -98,6 +141,48 @@ export const loadAllPlugins = async () => { plugin.dependenciesInstalled().forEach((missing) => missingDependencies.push({id:from,missing})) plugin.pluginsIncompatible(opendiscord.plugins).forEach((incompatible) => incompatibilities.push({from,to:incompatible})) plugin.pluginsInstalled(opendiscord.plugins).forEach((missing) => missingPlugins.push({id:from,missing})) + + // Check version compatibility + if (plugin.data.details.versions && plugin.data.details.versions.length > 0) { + const currentVersion = opendiscord.versions.get("opendiscord:version") + let isCompatible = false + + for (const versionStr of plugin.data.details.versions) { + // Parse version string (e.g., "OTv4.0.x" or "OTv4.1.2") + const match = versionStr.match(/^(OT|OD|OM|OU)v(\d+)\.(\d+)(?:\.(\d+|x))$/) + if (!match) continue + + const projectPrefix = match[1] + const primary = parseInt(match[2]) + const secondary = parseInt(match[3]) + const tertiary = match[4] + + // Only check OT (Open Ticket) versions for now + if (projectPrefix !== "OT") continue + + // Check if version matches + if (tertiary === "x") { + // Wildcard version (e.g., "OTv4.0.x" matches 4.0.0, 4.0.1, etc.) + if (currentVersion.primary === primary && currentVersion.secondary === secondary) { + isCompatible = true + break + } + } else { + // Exact version (e.g., "OTv4.0.0") + const requiredVersion = api.ODVersion.fromString("temp", `v${primary}.${secondary}.${parseInt(tertiary)}`) + if (currentVersion.primary === requiredVersion.primary && + currentVersion.secondary === requiredVersion.secondary && + currentVersion.tertiary === requiredVersion.tertiary) { + isCompatible = true + break + } + } + } + + if (!isCompatible) { + versionIncompatibilities.push({id:from}) + } + } }) //handle all incompatibilities @@ -152,6 +237,22 @@ export const loadAllPlugins = async () => { initPluginError = true }) + //handle all version incompatibilities + versionIncompatibilities.forEach((match) => { + const plugin = opendiscord.plugins.get(match.id) + if (plugin && !plugin.crashed){ + plugin.crashed = true + plugin.crashReason = "missing.dependency" // Reuse this reason for version incompatibility + } + + const versions = plugin?.data.details.versions?.join(", ") ?? "unknown" + const currentVersion = opendiscord.versions.get("opendiscord:version").toString() + opendiscord.log(`Plugin version incompatibility: plugin requires "${versions}" but current version is "${currentVersion}", canceling plugin execution...`,"plugin",[ + {key:"path",value:"./plugins/"+match.id} + ]) + initPluginError = true + }) + //exit on error (when soft mode disabled) if (!opendiscord.defaults.getDefault("softPluginLoading") && initPluginError){ console.log("") @@ -177,17 +278,21 @@ export const loadAllPlugins = async () => { } for (const plugin of sortedPlugins){ + // Get authors list (normalized to array) + const authors = (Array.isArray(plugin.details.author) ? plugin.details.author : + (plugin.details.authors || [plugin.details.author as string])).join(", ") + if (plugin.enabled){ opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" loaded",[ {key:"status",value:(plugin.crashed ? "crashed" : "success")}, {key:"crashReason",value:(plugin.crashed ? (plugin.crashReason ?? "/") : "/")}, - {key:"author",value:plugin.details.author}, + {key:"author",value:authors}, {key:"version",value:plugin.version.toString()}, {key:"priority",value:plugin.priority.toString()} ]) }else{ opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" disabled",[ - {key:"author",value:plugin.details.author}, + {key:"author",value:authors}, {key:"version",value:plugin.version.toString()}, {key:"priority",value:plugin.priority.toString()} ]) From 3989fb5f7613dc3ea81fcc5fc8cb31c1c2aaea16 Mon Sep 17 00:00:00 2001 From: Karanja-Stephen Date: Wed, 11 Feb 2026 10:44:51 +0300 Subject: [PATCH 2/3] refactor: simplify comments and make them less verbose --- index.js | 4 +--- src/core/api/modules/plugin.ts | 3 --- src/core/startup/pluginLauncher.ts | 21 +++------------------ 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index 5c5aa51..239aa77 100644 --- a/index.js +++ b/index.js @@ -84,7 +84,6 @@ function saveNewCompilationHash(){ } if (!process.argv.includes("--no-compile")){ - // Read plugin.json files before compilation to check for npm dependencies const pluginDependencies = new Set() if (fs.existsSync("./plugins")){ console.log("OT: Reading plugin.json files...") @@ -106,12 +105,11 @@ if (!process.argv.includes("--no-compile")){ }) } } catch (e) { - // Silently skip invalid plugin.json files - they'll be caught during plugin loading + // skip invalid plugin.json files, will be caught later } } } - // Check for missing dependencies if (pluginDependencies.size > 0){ console.log("OT: Checking plugin npm dependencies...") const missingDeps = [] diff --git a/src/core/api/modules/plugin.ts b/src/core/api/modules/plugin.ts index a2f4ac4..75390f6 100644 --- a/src/core/api/modules/plugin.ts +++ b/src/core/api/modules/plugin.ts @@ -219,7 +219,6 @@ export class ODPlugin extends ODManagerData { return incompatible } - /**Get all authors as an array. Handles both old (string) and new (array) format. */ getAuthors(): string[] { if (Array.isArray(this.details.author)) { return this.details.author @@ -231,12 +230,10 @@ export class ODPlugin extends ODManagerData { return [] } - /**Get all contributors as an array. */ getContributors(): string[] { return this.details.contributors || [] } - /**Get all compatible versions as an array. */ getCompatibleVersions(): string[] { return this.details.versions || [] } diff --git a/src/core/startup/pluginLauncher.ts b/src/core/startup/pluginLauncher.ts index 49d2c5e..e8d0517 100644 --- a/src/core/startup/pluginLauncher.ts +++ b/src/core/startup/pluginLauncher.ts @@ -48,41 +48,33 @@ export const loadAllPlugins = async () => { if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details") - // Handle author field - support both old (string) and new (array) format for backwards compatibility + // author can be string (old) or array (new), convert to array for consistency if (typeof rawplugindata.details.author != "string" && !Array.isArray(rawplugindata.details.author)) { throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)") } - // Normalize author to array format for internal use if (typeof rawplugindata.details.author == "string") { - // Old format: convert string to array rawplugindata.details.authors = [rawplugindata.details.author] } else if (Array.isArray(rawplugindata.details.author)) { - // New format: use author array as authors rawplugindata.details.authors = rawplugindata.details.author } - // Validate authors array if provided separately if (rawplugindata.details.authors && !Array.isArray(rawplugindata.details.authors)) { throw new ODPluginError("Failed to load plugin.json/details/authors (must be array)") } - // Validate contributors array if provided if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) { throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)") } - // Validate versions array if provided if (rawplugindata.details.versions) { if (!Array.isArray(rawplugindata.details.versions)) { throw new ODPluginError("Failed to load plugin.json/details/versions (must be array)") } - // Validate version format: should match pattern like "OTv4.0.x", "ODv1.0.0", etc. for (const version of rawplugindata.details.versions) { if (typeof version != "string") { throw new ODPluginError("Failed to load plugin.json/details/versions (all items must be strings)") } - // Check format: project prefix (OT, OD, OM, OU) + v + version pattern const versionPattern = /^(OT|OD|OM|OU)v\d+\.\d+(\.\d+|\.x)$/ if (!versionPattern.test(version)) { throw new ODPluginError(`Failed to load plugin.json/details/versions (invalid format: "${version}", expected format like "OTv4.0.x" or "ODv1.0.0")`) @@ -142,13 +134,12 @@ export const loadAllPlugins = async () => { plugin.pluginsIncompatible(opendiscord.plugins).forEach((incompatible) => incompatibilities.push({from,to:incompatible})) plugin.pluginsInstalled(opendiscord.plugins).forEach((missing) => missingPlugins.push({id:from,missing})) - // Check version compatibility + // check if plugin versions are compatible if (plugin.data.details.versions && plugin.data.details.versions.length > 0) { const currentVersion = opendiscord.versions.get("opendiscord:version") let isCompatible = false for (const versionStr of plugin.data.details.versions) { - // Parse version string (e.g., "OTv4.0.x" or "OTv4.1.2") const match = versionStr.match(/^(OT|OD|OM|OU)v(\d+)\.(\d+)(?:\.(\d+|x))$/) if (!match) continue @@ -157,18 +148,14 @@ export const loadAllPlugins = async () => { const secondary = parseInt(match[3]) const tertiary = match[4] - // Only check OT (Open Ticket) versions for now if (projectPrefix !== "OT") continue - // Check if version matches if (tertiary === "x") { - // Wildcard version (e.g., "OTv4.0.x" matches 4.0.0, 4.0.1, etc.) if (currentVersion.primary === primary && currentVersion.secondary === secondary) { isCompatible = true break } } else { - // Exact version (e.g., "OTv4.0.0") const requiredVersion = api.ODVersion.fromString("temp", `v${primary}.${secondary}.${parseInt(tertiary)}`) if (currentVersion.primary === requiredVersion.primary && currentVersion.secondary === requiredVersion.secondary && @@ -237,12 +224,11 @@ export const loadAllPlugins = async () => { initPluginError = true }) - //handle all version incompatibilities versionIncompatibilities.forEach((match) => { const plugin = opendiscord.plugins.get(match.id) if (plugin && !plugin.crashed){ plugin.crashed = true - plugin.crashReason = "missing.dependency" // Reuse this reason for version incompatibility + plugin.crashReason = "missing.dependency" } const versions = plugin?.data.details.versions?.join(", ") ?? "unknown" @@ -278,7 +264,6 @@ export const loadAllPlugins = async () => { } for (const plugin of sortedPlugins){ - // Get authors list (normalized to array) const authors = (Array.isArray(plugin.details.author) ? plugin.details.author : (plugin.details.authors || [plugin.details.author as string])).join(", ") From 138f7fa2bbd20962b40d0103b21ca9247fdf3fae Mon Sep 17 00:00:00 2001 From: DJj123dj <80536295+DJj123dj@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:37:28 +0100 Subject: [PATCH 3/3] Small Improvements for the new plugin.json code --- index.js | 32 ++++----- plugins/example-plugin/plugin.json | 3 +- src/core/api/modules/plugin.ts | 36 ++++------ src/core/startup/pluginLauncher.ts | 101 +++++++++++------------------ 4 files changed, 66 insertions(+), 106 deletions(-) diff --git a/index.js b/index.js index 239aa77..521f871 100644 --- a/index.js +++ b/index.js @@ -28,6 +28,7 @@ const fs = require("fs") const ts = require("typescript") const {createHash,Hash} = require("crypto") const nodepath = require('path') +const ansis = require("ansis") /** ## What is this? * This is a function which compares `./src/` with a hash stored in `./dist/hash.txt`. @@ -84,48 +85,47 @@ function saveNewCompilationHash(){ } if (!process.argv.includes("--no-compile")){ - const pluginDependencies = new Set() + const requiredDependencies = new Set() if (fs.existsSync("./plugins")){ console.log("OT: Reading plugin.json files...") - const plugins = fs.readdirSync("./plugins") - for (const pluginDir of plugins){ + for (const pluginDir of fs.readdirSync("./plugins")){ if (pluginDir === ".DS_Store") continue const pluginPath = nodepath.join("./plugins", pluginDir) if (!fs.statSync(pluginPath).isDirectory()) continue const pluginJsonPath = nodepath.join(pluginPath, "plugin.json") if (fs.existsSync(pluginJsonPath)){ - try { + try{ const pluginData = JSON.parse(fs.readFileSync(pluginJsonPath).toString()) if (pluginData.npmDependencies && Array.isArray(pluginData.npmDependencies)){ - pluginData.npmDependencies.forEach(dep => { - if (typeof dep === "string" && dep.trim()) { - pluginDependencies.add(dep.trim()) + pluginData.npmDependencies.forEach((dep) => { + if (typeof dep === "string" && dep.trim()){ + requiredDependencies.add(dep.trim()) } }) } - } catch (e) { + }catch(err){ // skip invalid plugin.json files, will be caught later } } } - if (pluginDependencies.size > 0){ + if (requiredDependencies.size > 0){ console.log("OT: Checking plugin npm dependencies...") + /**@type {string[]} */ const missingDeps = [] - for (const dep of pluginDependencies){ - try { + for (const dep of requiredDependencies){ + try{ require.resolve(dep) - } catch { + }catch(err){ missingDeps.push(dep) } } if (missingDeps.length > 0){ - console.log("OT: Warning - Missing npm dependencies required by plugins:") - missingDeps.forEach(dep => console.log(` - ${dep}`)) - console.log("OT: Please install missing dependencies with: npm install " + missingDeps.join(" ")) - console.log("OT: Continuing compilation anyway...") + console.log(ansis.red("OT: ❌ Fatal Error --> Missing npm dependencies required by plugins:\n\n")+ansis.cyan(missingDeps.map((dep) => " - "+dep).join("\n")+"\n")) + console.log("OT: Please install missing dependencies using the following command:\n> "+ansis.bold.green("npm install " + missingDeps.join(" "))+"\n") + process.exit(1) } } } diff --git a/plugins/example-plugin/plugin.json b/plugins/example-plugin/plugin.json index c8eae73..f09ed6a 100644 --- a/plugins/example-plugin/plugin.json +++ b/plugins/example-plugin/plugin.json @@ -3,6 +3,7 @@ "id":"example-plugin", "version":"1.0.0", "startFile":"index.ts", + "supportedVersions":["OTv4.0.x","OTv4.1.x"], "enabled":false, "priority":0, @@ -14,9 +15,7 @@ "details":{ "author":"DJj123dj", - "authors":["DJj123dj"], "contributors":[], - "versions":["OTv4.0.x","OTv4.1.x"], "shortDescription":"A simple template for an Open Ticket v4 plugin!", "longDescription":"A simple example of an Open Ticket v4 plugin!", "imageUrl":"", diff --git a/src/core/api/modules/plugin.ts b/src/core/api/modules/plugin.ts index 75390f6..81ae500 100644 --- a/src/core/api/modules/plugin.ts +++ b/src/core/api/modules/plugin.ts @@ -55,6 +55,11 @@ export interface ODPluginData { version:string, /**The location of the start file of the plugin relative to the rootDir of the plugin */ startFile:string, + /**A list of compatible versions. (e.g. `["OTv4.0.x", "OMv1.x.x"]`) (optional, will be required in future version) + * - `OT` --> Open Ticket support + * - `OM` --> Open Moderation support + */ + supportedVersions?:string[], /**Is this plugin enabled? */ enabled:boolean, @@ -78,14 +83,10 @@ export interface ODPluginData { * Additional details in the `plugin.json` file from a plugin. */ export interface ODPluginDetails { - /**The author of the plugin. (string for backwards compatibility, or string[] for multiple authors) */ - author:string|string[], - /**A list of authors of the plugin. (new format, optional if author is provided) */ - authors?:string[], - /**A list of contributors to the plugin. (optional) */ + /**The main author of the plugin. Additional contributors can be specified in `contributors`. */ + author:string, + /**A list of plugin contributors. (optional, will be required in future version) */ contributors?:string[], - /**A list of compatible versions. (e.g. ["OTv4.0.x", "OTv4.1.x", "ODv1.0.0"]) */ - versions?:string[], /**A short description of this plugin. */ shortDescription:string, /**A large description of this plugin. */ @@ -127,7 +128,7 @@ export class ODPlugin extends ODManagerData { /**Did this plugin crash? (A reason is available in the `crashReason`) */ crashed: boolean /**The reason which caused this plugin to crash. */ - crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"executed" = null + crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"incompatible.version"|"executed" = null constructor(dir:string, jsondata:ODPluginData){ super(jsondata.id) @@ -218,24 +219,9 @@ export class ODPlugin extends ODManagerData { return incompatible } - + /**Get a list of all authors & contributors of this plugin. */ getAuthors(): string[] { - if (Array.isArray(this.details.author)) { - return this.details.author - } else if (this.details.authors && Array.isArray(this.details.authors)) { - return this.details.authors - } else if (typeof this.details.author === "string") { - return [this.details.author] - } - return [] - } - - getContributors(): string[] { - return this.details.contributors || [] - } - - getCompatibleVersions(): string[] { - return this.details.versions || [] + return [this.details.author,...(this.details.contributors ?? [])] } } diff --git a/src/core/startup/pluginLauncher.ts b/src/core/startup/pluginLauncher.ts index e8d0517..e95a62c 100644 --- a/src/core/startup/pluginLauncher.ts +++ b/src/core/startup/pluginLauncher.ts @@ -12,6 +12,7 @@ export const loadAllPlugins = async () => { return } const plugins = fs.readdirSync("./plugins") + const pluginVersionRegex = /^(OT|OM)v(\d+)\.(\d+|x)\.(\d+|x)$/ //check & validate plugins.forEach((p) => { @@ -38,6 +39,20 @@ export const loadAllPlugins = async () => { if (typeof rawplugindata.version != "string") throw new ODPluginError("Failed to load plugin.json/version") if (typeof rawplugindata.startFile != "string") throw new ODPluginError("Failed to load plugin.json/startFile") + //only check "supportedVersions" if it exists (should be array) + if (rawplugindata.supportedVersions){ + if (!Array.isArray(rawplugindata.supportedVersions)) throw new ODPluginError("Failed to load plugin.json/supportedVersions (must be array)") + for (const version of rawplugindata.supportedVersions){ + if (typeof version !== "string"){ + throw new ODPluginError("Failed to load plugin.json/supportedVersions (all items must be strings)") + } + //only OT (Open Ticket) & OM (Open Moderation) are supported at the moment + if (!pluginVersionRegex.test(version)){ + throw new ODPluginError(`Failed to load plugin.json/supportedVersions (invalid format: "${version}", expected format like "OTv4.0.x" or "OMv1.0.0")`) + } + } + } + if (typeof rawplugindata.enabled != "boolean") throw new ODPluginError("Failed to load plugin.json/enabled") if (typeof rawplugindata.priority != "number") throw new ODPluginError("Failed to load plugin.json/priority") if (!Array.isArray(rawplugindata.events)) throw new ODPluginError("Failed to load plugin.json/events") @@ -47,40 +62,10 @@ export const loadAllPlugins = async () => { if (!Array.isArray(rawplugindata.incompatiblePlugins)) throw new ODPluginError("Failed to load plugin.json/incompatiblePlugins") if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details") + if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)") - // author can be string (old) or array (new), convert to array for consistency - if (typeof rawplugindata.details.author != "string" && !Array.isArray(rawplugindata.details.author)) { - throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)") - } - - if (typeof rawplugindata.details.author == "string") { - rawplugindata.details.authors = [rawplugindata.details.author] - } else if (Array.isArray(rawplugindata.details.author)) { - rawplugindata.details.authors = rawplugindata.details.author - } - - if (rawplugindata.details.authors && !Array.isArray(rawplugindata.details.authors)) { - throw new ODPluginError("Failed to load plugin.json/details/authors (must be array)") - } - - if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) { - throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)") - } - - if (rawplugindata.details.versions) { - if (!Array.isArray(rawplugindata.details.versions)) { - throw new ODPluginError("Failed to load plugin.json/details/versions (must be array)") - } - for (const version of rawplugindata.details.versions) { - if (typeof version != "string") { - throw new ODPluginError("Failed to load plugin.json/details/versions (all items must be strings)") - } - const versionPattern = /^(OT|OD|OM|OU)v\d+\.\d+(\.\d+|\.x)$/ - if (!versionPattern.test(version)) { - throw new ODPluginError(`Failed to load plugin.json/details/versions (invalid format: "${version}", expected format like "OTv4.0.x" or "ODv1.0.0")`) - } - } - } + //only check "contributors" if it exists (should be array) + if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)") if (typeof rawplugindata.details.shortDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/shortDescription") if (typeof rawplugindata.details.longDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/longDescription") @@ -134,41 +119,31 @@ export const loadAllPlugins = async () => { plugin.pluginsIncompatible(opendiscord.plugins).forEach((incompatible) => incompatibilities.push({from,to:incompatible})) plugin.pluginsInstalled(opendiscord.plugins).forEach((missing) => missingPlugins.push({id:from,missing})) - // check if plugin versions are compatible - if (plugin.data.details.versions && plugin.data.details.versions.length > 0) { + //check if plugins are compatible with version of bot + if (plugin.data.supportedVersions && plugin.data.supportedVersions.length > 0){ const currentVersion = opendiscord.versions.get("opendiscord:version") let isCompatible = false - for (const versionStr of plugin.data.details.versions) { - const match = versionStr.match(/^(OT|OD|OM|OU)v(\d+)\.(\d+)(?:\.(\d+|x))$/) + for (const versionStr of plugin.data.supportedVersions){ + const match = versionStr.match(pluginVersionRegex) if (!match) continue const projectPrefix = match[1] const primary = parseInt(match[2]) - const secondary = parseInt(match[3]) - const tertiary = match[4] + const secondary = (match[3] === "x") ? null : parseInt(match[3]) + const tertiary = (match[4] === "x") ? null : parseInt(match[4]) if (projectPrefix !== "OT") continue - - if (tertiary === "x") { - if (currentVersion.primary === primary && currentVersion.secondary === secondary) { - isCompatible = true - break - } - } else { - const requiredVersion = api.ODVersion.fromString("temp", `v${primary}.${secondary}.${parseInt(tertiary)}`) - if (currentVersion.primary === requiredVersion.primary && - currentVersion.secondary === requiredVersion.secondary && - currentVersion.tertiary === requiredVersion.tertiary) { - isCompatible = true - break - } + else if (primary !== currentVersion.primary) continue + else if (typeof secondary === "number" && secondary !== currentVersion.secondary) continue + else if (typeof tertiary === "number" && tertiary !== currentVersion.tertiary) continue + else{ + isCompatible = true + break } } - if (!isCompatible) { - versionIncompatibilities.push({id:from}) - } + if (!isCompatible) versionIncompatibilities.push({id:from}) } }) @@ -224,16 +199,17 @@ export const loadAllPlugins = async () => { initPluginError = true }) + //handle all bot version incompatibilities versionIncompatibilities.forEach((match) => { const plugin = opendiscord.plugins.get(match.id) if (plugin && !plugin.crashed){ plugin.crashed = true - plugin.crashReason = "missing.dependency" + plugin.crashReason = "incompatible.version" } - const versions = plugin?.data.details.versions?.join(", ") ?? "unknown" + const versions = plugin?.data.supportedVersions?.join(", ") ?? "" const currentVersion = opendiscord.versions.get("opendiscord:version").toString() - opendiscord.log(`Plugin version incompatibility: plugin requires "${versions}" but current version is "${currentVersion}", canceling plugin execution...`,"plugin",[ + opendiscord.log(`Plugin version incompatibility: plugin requires "${versions}" but current bot version is "${currentVersion}", canceling plugin execution...`,"plugin",[ {key:"path",value:"./plugins/"+match.id} ]) initPluginError = true @@ -264,20 +240,19 @@ export const loadAllPlugins = async () => { } for (const plugin of sortedPlugins){ - const authors = (Array.isArray(plugin.details.author) ? plugin.details.author : - (plugin.details.authors || [plugin.details.author as string])).join(", ") + const authors = [plugin.details.author,...(plugin.details.contributors ?? [])].join(", ") if (plugin.enabled){ opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" loaded",[ {key:"status",value:(plugin.crashed ? "crashed" : "success")}, {key:"crashReason",value:(plugin.crashed ? (plugin.crashReason ?? "/") : "/")}, - {key:"author",value:authors}, + {key:"authors",value:authors}, {key:"version",value:plugin.version.toString()}, {key:"priority",value:plugin.priority.toString()} ]) }else{ opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" disabled",[ - {key:"author",value:authors}, + {key:"authors",value:authors}, {key:"version",value:plugin.version.toString()}, {key:"priority",value:plugin.priority.toString()} ])