diff --git a/docs/plugins.md b/docs/plugins.md index 70fb617..52af73d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -7,17 +7,11 @@ The path is relative to the bot's folder. Plugins are automatically loaded on bot startup. ## Creating a plugin -Create a `.js` file that exports a function. -This function will be called when the plugin is loaded, with 1 argument: an object that has the following properties: -* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) -* `knex` - the [Knex database object](https://knexjs.org/#Builder) -* `config` - the loaded config -* `commands` - an object with functions to add and manage commands -* `attachments` - an object with functions to save attachments and manage attachment storage types +Plugins are simply `.js` files that export a function that gets called when the plugin is loaded. -See [src/plugins.js#L4](../src/plugins.js#L4) for more details +For details about the function arguments, see [Plugin API](#plugin-api) below. -### Example plugin file +### Example plugin This example adds a command `!mycommand` that replies with `"Reply from my custom plugin!"` when the command is used inside a modmail inbox thread channel. ```js module.exports = function({ bot, knex, config, commands }) { @@ -40,6 +34,36 @@ module.exports = function({ attachments }) { ``` To use this custom attachment storage type, you would set the `attachmentStorage` config option to `"original"`. +### Plugin API +The first and only argument to the plugin function is an object with the following properties: + +| Property | Description | +| -------- | ----------- | +| `bot` | [Eris Client instance](https://abal.moe/Eris/docs/Client) | +| `knex` | [Knex database object](https://knexjs.org/#Builder) | +| `config` | The loaded config | +| `commands` | An object with functions to add and manage commands | +| `attachments` | An object with functions to save attachments and manage attachment storage types | +| — `addStorageType(name, handler)` | Function to add a new attachment storage type | +| — `downloadAttachment(attachment)` | Function to add a new attachment storage type | + +* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) +* `knex` - the [Knex database object](https://knexjs.org/#Builder) +* `config` - the loaded config +* `commands` - an object with functions to add and manage commands +* `attachments` - an object with functions to save attachments and manage attachment storage types + * `attachments.addStorageType(name, handler)` + +Create a `.js` file that exports a function. +This function will be called when the plugin is loaded, with 1 argument: an object that has the following properties: +* `bot` - the [Eris Client object](https://abal.moe/Eris/docs/Client) +* `knex` - the [Knex database object](https://knexjs.org/#Builder) +* `config` - the loaded config +* `commands` - an object with functions to add and manage commands +* `attachments` - an object with functions to save attachments and manage attachment storage types + +See [src/plugins.js#L4](../src/plugins.js#L4) for more details + ## Work in progress The current plugin API is fairly rudimentary and will be expanded on in the future. The API can change in non-major releases during this early stage. Keep an eye on [CHANGELOG.md](../CHANGELOG.md) for any changes. diff --git a/src/commands.js b/src/commands.js index cdc95de..a0c34e2 100644 --- a/src/commands.js +++ b/src/commands.js @@ -5,6 +5,51 @@ const utils = require("./utils"); const threads = require("./data/threads"); const Thread = require("./data/Thread"); +/** + * @callback CommandFn + * @param {Eris.Message} msg + * @param {object} args + */ + +/** + * @callback InboxThreadCommandHandler + * @param {Eris.Message} msg + * @param {object} args + * @param {Thread} thread + */ + +/** + * @callback AddGlobalCommandFn + * @param {string} trigger + * @param {string} parameters + * @param {CommandFn} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddInboxServerCommandFn + * @param {string} trigger + * @param {string} parameters + * @param {CommandFn} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddInboxThreadCommandFn + * Add a command that can only be invoked in a thread on the inbox server + * + * @param {string} trigger + * @param {string} parameters + * @param {InboxThreadCommandHandler} handler + * @param {ICommandConfig} commandConfig + */ + +/** + * @callback AddAliasFn + * @param {string} originalCmd + * @param {string} alias + */ + module.exports = { createCommandManager(bot) { const manager = new CommandManager({ @@ -52,6 +97,7 @@ module.exports = { /** * Add a command that can be invoked anywhere + * @type {AddGlobalCommandFn} */ const addGlobalCommand = (trigger, parameters, handler, commandConfig = {}) => { let aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -63,6 +109,7 @@ module.exports = { /** * Add a command that can only be invoked on the inbox server + * @type {AddInboxServerCommandFn} */ const addInboxServerCommand = (trigger, parameters, handler, commandConfig = {}) => { const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -86,19 +133,9 @@ module.exports = { }; }; - /** - * @callback InboxThreadCommandHandler - * @param {Eris.Message} msg - * @param {object} args - * @param {Thread} thread - */ - /** * Add a command that can only be invoked in a thread on the inbox server - * @param {string|RegExp} trigger - * @param {string|IParameter[]} parameters - * @param {InboxThreadCommandHandler} handler - * @param {ICommandConfig} commandConfig + * @type {AddInboxThreadCommandFn} */ const addInboxThreadCommand = (trigger, parameters, handler, commandConfig = {}) => { const aliases = aliasMap.has(trigger) ? [...aliasMap.get(trigger)] : []; @@ -124,6 +161,9 @@ module.exports = { }; }; + /** + * @type {AddAliasFn} + */ const addAlias = (originalCmd, alias) => { if (! aliasMap.has(originalCmd)) { aliasMap.set(originalCmd, new Set()); diff --git a/src/data/attachments.js b/src/data/attachments.js index c26d056..a5392c0 100644 --- a/src/data/attachments.js +++ b/src/data/attachments.js @@ -26,21 +26,54 @@ function getErrorResult(msg = null) { } /** - * An attachment storage option that simply forwards the original attachment URL - * @param {Eris.Attachment} attachment - * @returns {{url: string}} + * @callback AddAttachmentStorageTypeFn + * @param {string} name + * @param {AttachmentStorageTypeHandler} handler */ -function passthroughOriginalAttachment(attachment) { + +/** + * @callback AttachmentStorageTypeHandler + * @param {Eris.Attachment} attachment + * @return {AttachmentStorageTypeResult|Promise} + */ + +/** + * @typedef {object} AttachmentStorageTypeResult + * @property {string} url + */ + +/** + * @callback DownloadAttachmentFn + * @param {Eris.Attachment} attachment + * @param {number?} tries Used internally, don't pass + * @return {Promise} + */ + +/** + * @typedef {object} DownloadAttachmentResult + * @property {string} path + * @property {DownloadAttachmentCleanupFn} cleanup + */ + +/** + * @callback DownloadAttachmentCleanupFn + * @return {void} + */ + +/** + * @type {AttachmentStorageTypeHandler} + */ +let passthroughOriginalAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +passthroughOriginalAttachment = (attachment) => { return { url: attachment.url }; -} +}; /** * An attachment storage option that downloads each attachment and serves them from a local web server - * @param {Eris.Attachment} attachment - * @param {Number=0} tries - * @returns {Promise<{ url: string }>} + * @type {AttachmentStorageTypeHandler} */ -async function saveLocalAttachment(attachment) { +let saveLocalAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +saveLocalAttachment = async (attachment) => { const targetPath = getLocalAttachmentPath(attachment.id); try { @@ -60,14 +93,12 @@ async function saveLocalAttachment(attachment) { const url = await getLocalAttachmentUrl(attachment.id, attachment.filename); return { url }; -} +}; /** - * @param {Eris.Attachment} attachment - * @param {Number} tries - * @returns {Promise<{ path: string, cleanup: function }>} + * @type {DownloadAttachmentFn} */ -function downloadAttachment(attachment, tries = 0) { +const downloadAttachment = (attachment, tries = 0) => { return new Promise((resolve, reject) => { if (tries > 3) { console.error("Attachment download failed after 3 tries:", attachment); @@ -94,7 +125,7 @@ function downloadAttachment(attachment, tries = 0) { }); }); }); -} +}; /** * Returns the filesystem path for the given attachment id @@ -119,10 +150,10 @@ function getLocalAttachmentUrl(attachmentId, desiredName = null) { /** * An attachment storage option that downloads each attachment and re-posts them to a specified Discord channel. * The re-posted attachment is then linked in the actual thread. - * @param {Eris.Attachment} attachment - * @returns {Promise<{ url: string }>} + * @type {AttachmentStorageTypeHandler} */ -async function saveDiscordAttachment(attachment) { +let saveDiscordAttachment; // Workaround to inconsistent IDE bug with @type and anonymous functions +saveDiscordAttachment = async (attachment) => { if (attachment.size > 1024 * 1024 * 8) { return getErrorResult("attachment too large (max 8MB)"); } @@ -144,7 +175,7 @@ async function saveDiscordAttachment(attachment) { if (! savedAttachment) return getErrorResult(); return { url: savedAttachment.url }; -} +}; async function createDiscordAttachmentMessage(channel, file, tries = 0) { tries++; @@ -197,9 +228,12 @@ function saveAttachment(attachment) { return attachmentSavePromises[attachment.id]; } -function addStorageType(name, handler) { +/** + * @type AddAttachmentStorageTypeFn + */ +const addStorageType = (name, handler) => { attachmentStorageTypes[name] = handler; -} +}; addStorageType("original", passthroughOriginalAttachment); addStorageType("local", saveLocalAttachment); diff --git a/src/formatters.js b/src/formatters.js index e329685..6693276 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -212,6 +212,20 @@ const defaultFormatters = { */ const formatters = { ...defaultFormatters }; +/** + * @typedef {object} FormattersExport + * @property {MessageFormatters} formatters Read only + * @property {function(FormatStaffReplyDM): void} setStaffReplyDMFormatter + * @property {function(FormatStaffReplyThreadMessage): void} setStaffReplyThreadMessageFormatter + * @property {function(FormatUserReplyThreadMessage): void} setUserReplyThreadMessageFormatter + * @property {function(FormatStaffReplyEditNotificationThreadMessage): void} setStaffReplyEditNotificationThreadMessageFormatter + * @property {function(FormatStaffReplyDeletionNotificationThreadMessage): void} setStaffReplyDeletionNotificationThreadMessageFormatter + * @property {function(FormatLog): void} setLogFormatter + */ + +/** + * @type {FormattersExport} + */ module.exports = { formatters: new Proxy(formatters, { set() { @@ -219,50 +233,26 @@ module.exports = { }, }), - /** - * @param {FormatStaffReplyDM} fn - * @return {void} - */ setStaffReplyDMFormatter(fn) { formatters.formatStaffReplyDM = fn; }, - /** - * @param {FormatStaffReplyThreadMessage} fn - * @return {void} - */ setStaffReplyThreadMessageFormatter(fn) { formatters.formatStaffReplyThreadMessage = fn; }, - /** - * @param {FormatUserReplyThreadMessage} fn - * @return {void} - */ setUserReplyThreadMessageFormatter(fn) { formatters.formatUserReplyThreadMessage = fn; }, - /** - * @param {FormatStaffReplyEditNotificationThreadMessage} fn - * @return {void} - */ setStaffReplyEditNotificationThreadMessageFormatter(fn) { formatters.formatStaffReplyEditNotificationThreadMessage = fn; }, - /** - * @param {FormatStaffReplyDeletionNotificationThreadMessage} fn - * @return {void} - */ setStaffReplyDeletionNotificationThreadMessageFormatter(fn) { formatters.formatStaffReplyDeletionNotificationThreadMessage = fn; }, - /** - * @param {FormatLog} fn - * @return {void} - */ setLogFormatter(fn) { formatters.formatLog = fn; }, diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js index 9805843..e0173aa 100644 --- a/src/hooks/beforeNewThread.js +++ b/src/hooks/beforeNewThread.js @@ -26,17 +26,24 @@ const Eris = require("eris"); * @return {void|Promise} */ +/** + * @callback AddBeforeNewThreadHookFn + * @param {BeforeNewThreadHookData} fn + * @return {void} + */ + /** * @type BeforeNewThreadHookData[] */ const beforeNewThreadHooks = []; /** - * @param {BeforeNewThreadHookData} fn + * @type {AddBeforeNewThreadHookFn} */ -function beforeNewThread(fn) { +let beforeNewThread; // Workaround to inconsistent IDE bug with @type and anonymous functions +beforeNewThread = (fn) => { beforeNewThreadHooks.push(fn); -} +}; /** * @param {{ diff --git a/src/pluginApi.js b/src/pluginApi.js new file mode 100644 index 0000000..65d8227 --- /dev/null +++ b/src/pluginApi.js @@ -0,0 +1,34 @@ +const { CommandManager } = require("knub-command-manager"); +const { Client } = require("eris"); +const Knex = require("knex"); + +/** + * @typedef {object} PluginAPI + * @property {Client} bot + * @property {Knex} knex + * @property {ModmailConfig} config + * @property {PluginCommandsAPI} commands + * @property {PluginAttachmentsAPI} attachments + * @property {PluginHooksAPI} hooks + * @property {FormattersExport} formats + */ + +/** + * @typedef {object} PluginCommandsAPI + * @property {CommandManager} manager + * @property {AddGlobalCommandFn} addGlobalCommand + * @property {AddInboxServerCommandFn} addInboxServerCommand + * @property {AddInboxThreadCommandFn} addInboxThreadCommand + * @property {AddAliasFn} addAlias + */ + +/** + * @typedef {object} PluginAttachmentsAPI + * @property {AddAttachmentStorageTypeFn} addStorageType + * @property {DownloadAttachmentFn} downloadAttachment + */ + +/** + * @typedef {object} PluginHooksAPI + * @property {AddBeforeNewThreadHookFn} beforeNewThread + */ diff --git a/src/plugins.js b/src/plugins.js index d03c2ae..f27cf50 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -3,6 +3,13 @@ const { beforeNewThread } = require("./hooks/beforeNewThread"); const formats = require("./formatters"); module.exports = { + /** + * @param bot + * @param knex + * @param config + * @param commands + * @returns {PluginAPI} + */ getPluginAPI({ bot, knex, config, commands }) { return { bot,