diff --git a/src/config.js b/src/config.js index bae358f..99f56e9 100644 --- a/src/config.js +++ b/src/config.js @@ -52,6 +52,7 @@ const defaultConfig = { "status": "Message me for help!", "responseMessage": "Thank you for your message! Our mod team will reply to you here as soon as possible.", "closeMessage": null, + "allowUserClose": false, "newThreadCategoryId": null, "mentionRole": "here", diff --git a/src/data/Thread.js b/src/data/Thread.js index e92a805..334609a 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -204,7 +204,7 @@ class Thread { } catch (e) { // Channel not found if (e.code === 10003) { - console.log(`[INFO] Auto-closing thread with ${this.user_name} because the channel no longer exists`); + console.log(`[INFO] Failed to send message to thread channel for ${this.user_name} because the channel no longer exists. Auto-closing the thread.`); this.close(true); } else { throw e; @@ -319,7 +319,7 @@ class Thread { async close(silent = false) { if (! silent) { console.log(`Closing thread ${this.id}`); - await this.postToThreadChannel('Closing thread...'); + await this.postSystemMessage('Closing thread...'); } // Update DB status diff --git a/src/main.js b/src/main.js index be1344a..37e698d 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,7 @@ const Eris = require('eris'); const config = require('./config'); const bot = require('./bot'); -const Queue = require('./queue'); +const {messageQueue} = require('./queue'); const utils = require('./utils'); const blocked = require('./data/blocked'); const threads = require('./data/threads'); @@ -25,8 +25,6 @@ const alert = require('./modules/alert'); const attachments = require("./data/attachments"); const {ACCIDENTAL_THREAD_MESSAGES} = require('./data/constants'); -const messageQueue = new Queue(); - // Once the bot has connected, set the status/"playing" message bot.on('ready', () => { bot.editStatus(null, {name: config.status}); diff --git a/src/modules/close.js b/src/modules/close.js index 0ba7263..05594ac 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -2,12 +2,12 @@ const humanizeDuration = require('humanize-duration'); const moment = require('moment'); const Eris = require('eris'); const config = require('../config'); -const threadUtils = require('../threadUtils'); -const utils = require("../utils"); -const threads = require("../data/threads"); +const utils = require('../utils'); +const threads = require('../data/threads'); +const blocked = require('../data/blocked'); +const {messageQueue} = require('../queue'); module.exports = bot => { - const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts)); // Check for threads that are scheduled to be closed and close them @@ -38,42 +38,71 @@ module.exports = bot => { scheduledCloseLoop(); // Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel. - addInboxServerCommand('close', async (msg, args, thread) => { - if (! thread) return; + bot.registerCommand('close', async (msg, args) => { + let thread, closedBy; - // Timed close - if (args.length) { - if (args[0].startsWith('c')) { - // Cancel timed close - if (thread.scheduled_close_at) { - await thread.cancelScheduledClose(); - thread.postSystemMessage(`Cancelled scheduled closing`); + if (msg.channel instanceof Eris.PrivateChannel) { + // User is closing the thread by themselves (if enabled) + if (! config.allowUserClose) return; + if (await blocked.isBlocked(msg.author.id)) return; + + thread = await threads.findOpenThreadByUserId(msg.author.id); + if (! thread) return; + + // We need to add this operation to the message queue so we don't get a race condition + // between showing the close command in the thread and closing the thread + await messageQueue.add(async () => { + thread.postSystemMessage('Thread closed by user, closing...'); + await thread.close(true); + }); + + closedBy = 'the user'; + } else { + // A staff member is closing the thread + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg.member)) return; + + thread = await threads.findOpenThreadByChannelId(msg.channel.id); + if (! thread) return; + + // Timed close + if (args.length) { + if (args[0].startsWith('c')) { + // Cancel timed close + if (thread.scheduled_close_at) { + await thread.cancelScheduledClose(); + thread.postSystemMessage(`Cancelled scheduled closing`); + } + + return; } + // Set a timed close + const delay = utils.convertDelayStringToMS(args.join(' ')); + if (delay === 0 || delay === null) { + thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); + return; + } + + const closeAt = moment.utc().add(delay, 'ms'); + await thread.scheduleClose(closeAt.format('YYYY-MM-DD HH:mm:ss'), msg.author); + thread.postSystemMessage(`Thread is now scheduled to be closed in ${humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`); + return; } - // Set a timed close - const delay = utils.convertDelayStringToMS(args.join(' ')); - if (delay === 0 || delay === null) { - thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); - return; - } - - const closeAt = moment.utc().add(delay, 'ms'); - await thread.scheduleClose(closeAt.format('YYYY-MM-DD HH:mm:ss'), msg.author); - thread.postSystemMessage(`Thread is now scheduled to be closed in ${humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`); - - return; + // Regular close + await thread.close(); + closedBy = msg.author.username; } - // Regular close - if(config.closeMessage) await thread.postToUser(config.closeMessage).catch(() => {}); - await thread.close(); + if (config.closeMessage) { + await thread.postToUser(config.closeMessage).catch(() => {}); + } const logUrl = await thread.getLogUrl(); utils.postLog(utils.trimAll(` - Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${msg.author.username} + Modmail thread with ${thread.user_name} (${thread.user_id}) was closed by ${closedBy} Logs: ${logUrl} `)); }); @@ -82,11 +111,12 @@ module.exports = bot => { bot.on('channelDelete', async (channel) => { if (! (channel instanceof Eris.TextChannel)) return; if (channel.guild.id !== utils.getInboxGuild().id) return; + const thread = await threads.findOpenThreadByChannelId(channel.id); if (! thread) return; console.log(`[INFO] Auto-closing thread with ${thread.user_name} because the channel was deleted`); - if(config.closeMessage) await thread.postToUser(config.closeMessage).catch(() => {}); + if (config.closeMessage) await thread.postToUser(config.closeMessage).catch(() => {}); await thread.close(true); const logUrl = await thread.getLogUrl(); diff --git a/src/queue.js b/src/queue.js index 32dba74..589425e 100644 --- a/src/queue.js +++ b/src/queue.js @@ -5,8 +5,16 @@ class Queue { } add(fn) { - this.queue.push(fn); - if (! this.running) this.next(); + const promise = new Promise(resolve => { + this.queue.push(async () => { + await Promise.resolve(fn()); + resolve(); + }); + + if (! this.running) this.next(); + }); + + return promise; } next() { @@ -20,10 +28,13 @@ class Queue { const fn = this.queue.shift(); new Promise(resolve => { // Either fn() completes or the timeout of 10sec is reached - Promise.resolve(fn()).then(resolve); + fn().then(resolve); setTimeout(resolve, 10000); }).then(() => this.next()); } } -module.exports = Queue; +module.exports = { + Queue, + messageQueue: new Queue() +};