diff --git a/src/data/threads.js b/src/data/threads.js index aac6bf2..2fefa7a 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -12,6 +12,7 @@ const utils = require('../utils'); const updates = require('./updates'); const Thread = require('./Thread'); +const {callBeforeNewThreadHooks} = require("../hooks/beforeNewThread"); const {THREAD_STATUS} = require('./constants'); const MINUTES = 60 * 1000; @@ -49,13 +50,17 @@ function getHeaderGuildInfo(member) { }; } +/** + * @typedef CreateNewThreadForUserOpts + * @property {boolean} quiet If true, doesn't ping mentionRole or reply with responseMessage + * @property {boolean} ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge + * @property {string} source A string identifying the source of the new thread + */ + /** * Creates a new modmail thread for the specified user * @param {User} user - * @param {Object} opts - * @param {Boolean} opts.quiet If true, doesn't ping mentionRole or reply with responseMessage - * @param {Boolean} opts.ignoreRequirements If true, creates a new thread even if the account doesn't meet requiredAccountAge - * @param {String} opts.categoryId Override the category ID for the new thread + * @param {CreateNewThreadForUserOpts} opts * @returns {Promise} * @throws {Error} */ @@ -68,6 +73,9 @@ async function createNewThreadForUser(user, opts = {}) { throw new Error('Attempted to create a new thread for a user with an existing open thread!'); } + const hookResult = await callBeforeNewThreadHooks({ user, opts }); + if (hookResult.cancelled) return; + // If set in config, check that the user's account is old enough (time since they registered on Discord) // If the account is too new, don't start a new thread and optionally reply to them with a message if (config.requiredAccountAge && ! ignoreRequirements) { @@ -131,7 +139,7 @@ async function createNewThreadForUser(user, opts = {}) { console.log(`[NOTE] Creating new thread channel ${channelName}`); // Figure out which category we should place the thread channel in - let newThreadCategoryId = opts.categoryId || null; + let newThreadCategoryId = hookResult.categoryId || null; if (! newThreadCategoryId && config.categoryAutomation.newThreadFromGuild) { // Categories for specific source guilds (in case of multiple main guilds) @@ -340,11 +348,16 @@ async function getClosedThreadCountByUserId(userId) { return parseInt(row.thread_count, 10); } -async function findOrCreateThreadForUser(user) { +/** + * @param {User} user + * @param {CreateNewThreadForUserOpts} opts + * @returns {Promise} + */ +async function findOrCreateThreadForUser(user, opts = {}) { const existingThread = await findOpenThreadByUserId(user.id); if (existingThread) return existingThread; - return createNewThreadForUser(user); + return createNewThreadForUser(user, opts); } async function getThreadsThatShouldBeClosed() { diff --git a/src/hooks.js b/src/hooks.js deleted file mode 100644 index 1896177..0000000 --- a/src/hooks.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @callback BeforeNewThreadHook_SetCategoryId - * @param {String} categoryId - * @return void - */ - -/** - * @typedef BeforeNewThreadHookEvent - * @property {Function} cancel - * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId - * - */ - -/** - * @callback BeforeNewThreadHook - * @param {BeforeNewThreadHookEvent} ev - * @return {void|Promise} - */ - -/** - * @type BeforeNewThreadHook[] - */ -const beforeNewThreadHooks = []; - -/** - * @param {BeforeNewThreadHook} fn - */ -function beforeNewThread(fn) { - beforeNewThreadHooks.push(fn); -} - -module.exports = { - beforeNewThreadHooks, - beforeNewThread, -}; diff --git a/src/hooks/beforeNewThread.js b/src/hooks/beforeNewThread.js new file mode 100644 index 0000000..801d84d --- /dev/null +++ b/src/hooks/beforeNewThread.js @@ -0,0 +1,82 @@ +const Eris = require('eris'); + +/** + * @callback BeforeNewThreadHook_SetCategoryId + * @param {String} categoryId + * @return void + */ + +/** + * @typedef BeforeNewThreadHookData + * @property {Eris.User} user + * @property {CreateNewThreadForUserOpts} opts + * @property {Function} cancel + * @property {BeforeNewThreadHook_SetCategoryId} setCategoryId + */ + +/** + * @typedef BeforeNewThreadHookResult + * @property {boolean} cancelled + * @property {string|null} categoryId + */ + +/** + * @callback BeforeNewThreadHookData + * @param {BeforeNewThreadHookData} data + * @return {void|Promise} + */ + +/** + * @type BeforeNewThreadHookData[] + */ +const beforeNewThreadHooks = []; + +/** + * @param {BeforeNewThreadHookData} fn + */ +function beforeNewThread(fn) { + beforeNewThreadHooks.push(fn); +} + +/** + * @param {{ + * user: Eris.User, + * opts: CreateNewThreadForUserOpts, + * }} input + * @return {Promise} + */ +async function callBeforeNewThreadHooks(input) { + /** + * @type {BeforeNewThreadHookResult} + */ + const result = { + cancelled: false, + categoryId: null, + }; + + /** + * @type {BeforeNewThreadHookData} + */ + const data = { + ...input, + + cancel() { + result.cancelled = true; + }, + + setCategoryId(value) { + result.categoryId = value; + }, + }; + + for (const hook of beforeNewThreadHooks) { + await hook(data); + } + + return result; +} + +module.exports = { + beforeNewThread, + callBeforeNewThreadHooks, +}; diff --git a/src/main.js b/src/main.js index 494805e..381efd8 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,7 @@ const {messageQueue} = require('./queue'); const utils = require('./utils'); const { createCommandManager } = require('./commands'); const { getPluginAPI, loadPlugin } = require('./plugins'); -const { beforeNewThreadHooks } = require('./hooks'); +const { callBeforeNewThreadHooks } = require('./hooks/beforeNewThread'); const blocked = require('./data/blocked'); const threads = require('./data/threads'); @@ -132,28 +132,9 @@ function initBaseMessageHandlers() { // Ignore messages that shouldn't usually open new threads, such as "ok", "thanks", etc. if (config.ignoreAccidentalThreads && msg.content && ACCIDENTAL_THREAD_MESSAGES.includes(msg.content.trim().toLowerCase())) return; - let cancelled = false; - let categoryId = null; - - /** - * @type {BeforeNewThreadHookEvent} - */ - const ev = { - cancel() { - cancelled = true; - }, - - setCategoryId(_categoryId) { - categoryId = _categoryId; - }, - }; - - for (const hook of beforeNewThreadHooks) { - await hook(ev, msg); - if (cancelled) return; - } - - thread = await threads.createNewThreadForUser(msg.author, { categoryId }); + thread = await threads.createNewThreadForUser(msg.author, { + source: 'dm', + }); } if (thread) { diff --git a/src/modules/newthread.js b/src/modules/newthread.js index 35f70d8..834ba55 100644 --- a/src/modules/newthread.js +++ b/src/modules/newthread.js @@ -15,7 +15,12 @@ module.exports = ({ bot, knex, config, commands }) => { return; } - const createdThread = await threads.createNewThreadForUser(user, { quiet: true, ignoreRequirements: true }); + const createdThread = await threads.createNewThreadForUser(user, { + quiet: true, + ignoreRequirements: true, + source: 'command', + }); + createdThread.postSystemMessage(`Thread was opened by ${msg.author.username}#${msg.author.discriminator}`); msg.channel.createMessage(`Thread opened: <#${createdThread.channel_id}>`); diff --git a/src/plugins.js b/src/plugins.js index 7cf3a7f..b078dbb 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -1,5 +1,5 @@ const attachments = require('./data/attachments'); -const { beforeNewThread } = require('./hooks'); +const { beforeNewThread } = require('./hooks/beforeNewThread'); const formats = require('./formatters'); module.exports = {