diff --git a/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js new file mode 100644 index 0000000..a22b5f3 --- /dev/null +++ b/db/migrations/20190306211534_add_scheduled_suspend_to_threads.js @@ -0,0 +1,15 @@ +exports.up = async function(knex, Promise) { + await knex.schema.table('threads', table => { + table.dateTime('scheduled_suspend_at').index().nullable().defaultTo(null).after('channel_id'); + table.string('scheduled_suspend_id', 20).nullable().defaultTo(null).after('channel_id'); + table.string('scheduled_suspend_name', 128).nullable().defaultTo(null).after('channel_id'); + }); +}; + +exports.down = async function(knex, Promise) { + await knex.schema.table('threads', table => { + table.dropColumn('scheduled_suspend_at'); + table.dropColumn('scheduled_suspend_id'); + table.dropColumn('scheduled_suspend_name'); + }); +}; diff --git a/src/data/Thread.js b/src/data/Thread.js index 0756c52..a95b1a5 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -356,6 +356,7 @@ class Thread { /** * @param {String} time * @param {Eris~User} user + * @param {Number} silent * @returns {Promise} */ async scheduleClose(time, user, silent) { @@ -390,7 +391,10 @@ class Thread { await knex('threads') .where('id', this.id) .update({ - status: THREAD_STATUS.SUSPENDED + status: THREAD_STATUS.SUSPENDED, + scheduled_suspend_at: null, + scheduled_suspend_id: null, + scheduled_suspend_name: null }); } @@ -405,6 +409,34 @@ class Thread { }); } + /** + * @param {String} time + * @param {Eris~User} user + * @returns {Promise} + */ + async scheduleSuspend(time, user) { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_suspend_at: time, + scheduled_suspend_id: user.id, + scheduled_suspend_name: user.username + }); + } + + /** + * @returns {Promise} + */ + async cancelScheduledSuspend() { + await knex('threads') + .where('id', this.id) + .update({ + scheduled_suspend_at: null, + scheduled_suspend_id: null, + scheduled_suspend_name: null + }); + } + /** * @param {String} userId * @returns {Promise} diff --git a/src/data/threads.js b/src/data/threads.js index baa9abe..d14c53d 100644 --- a/src/data/threads.js +++ b/src/data/threads.js @@ -273,6 +273,18 @@ async function getThreadsThatShouldBeClosed() { return threads.map(thread => new Thread(thread)); } +async function getThreadsThatShouldBeSuspended() { + const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const threads = await knex('threads') + .where('status', THREAD_STATUS.OPEN) + .whereNotNull('scheduled_suspend_at') + .where('scheduled_suspend_at', '<=', now) + .whereNotNull('scheduled_suspend_at') + .select(); + + return threads.map(thread => new Thread(thread)); +} + module.exports = { findById, findOpenThreadByUserId, @@ -283,5 +295,6 @@ module.exports = { getClosedThreadsByUserId, findOrCreateThreadForUser, getThreadsThatShouldBeClosed, + getThreadsThatShouldBeSuspended, createThreadInDB }; diff --git a/src/modules/close.js b/src/modules/close.js index 4abf96d..acedc62 100644 --- a/src/modules/close.js +++ b/src/modules/close.js @@ -1,4 +1,3 @@ -const humanizeDuration = require('humanize-duration'); const moment = require('moment'); const Eris = require('eris'); const config = require('../config'); @@ -8,8 +7,6 @@ const blocked = require('../data/blocked'); const {messageQueue} = require('../queue'); module.exports = bot => { - const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts)); - // Check for threads that are scheduled to be closed and close them async function applyScheduledCloses() { const threadsToBeClosed = await threads.getThreadsThatShouldBeClosed(); @@ -90,7 +87,6 @@ module.exports = bot => { // Timed close const delayStringArg = args.find(arg => utils.delayStringRegex.test(arg)); if (delayStringArg) { - // Set a timed close const delay = utils.convertDelayStringToMS(delayStringArg); if (delay === 0 || delay === null) { thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); @@ -102,9 +98,9 @@ module.exports = bot => { let response; if (silentClose) { - response = `Thread is now scheduled to be closed silently in ${humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; + response = `Thread is now scheduled to be closed silently in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; } else { - response = `Thread is now scheduled to be closed in ${humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; + response = `Thread is now scheduled to be closed in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}close cancel\` to cancel.`; } thread.postSystemMessage(response); diff --git a/src/modules/suspend.js b/src/modules/suspend.js index 7ede6b5..ed05630 100644 --- a/src/modules/suspend.js +++ b/src/modules/suspend.js @@ -1,11 +1,70 @@ +const moment = require('moment'); const threadUtils = require('../threadUtils'); const threads = require("../data/threads"); +const utils = require('../utils'); +const config = require('../config'); + +const {THREAD_STATUS} = require('../data/constants'); module.exports = bot => { const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); + // Check for threads that are scheduled to be suspended and suspend them + async function applyScheduledSuspensions() { + const threadsToBeSuspended = await threads.getThreadsThatShouldBeSuspended(); + for (const thread of threadsToBeSuspended) { + if (thread.status === THREAD_STATUS.OPEN) { + await thread.suspend(); + await thread.postSystemMessage(`**Thread suspended** as scheduled by ${thread.scheduled_suspend_name}. This thread will act as closed until unsuspended with \`!unsuspend\``); + } + } + } + + async function scheduledSuspendLoop() { + try { + await applyScheduledSuspensions(); + } catch (e) { + console.error(e); + } + + setTimeout(scheduledSuspendLoop, 2000); + } + + scheduledSuspendLoop(); + addInboxServerCommand('suspend', async (msg, args, thread) => { if (! thread) return; + + if (args.length) { + // Cancel timed suspend + if (args.includes('cancel') || args.includes('c')) { + // Cancel timed suspend + if (thread.scheduled_suspend_at) { + await thread.cancelScheduledSuspend(); + thread.postSystemMessage(`Cancelled scheduled suspension`); + } + + return; + } + + // Timed suspend + const delayStringArg = args.find(arg => utils.delayStringRegex.test(arg)); + if (delayStringArg) { + const delay = utils.convertDelayStringToMS(delayStringArg); + if (delay === 0 || delay === null) { + thread.postSystemMessage(`Invalid delay specified. Format: "1h30m"`); + return; + } + + const suspendAt = moment.utc().add(delay, 'ms'); + await thread.scheduleSuspend(suspendAt.format('YYYY-MM-DD HH:mm:ss'), msg.author); + + thread.postSystemMessage(`Thread will be suspended in ${utils.humanizeDelay(delay)}. Use \`${config.prefix}suspend cancel\` to cancel.`); + + return; + } + } + await thread.suspend(); thread.postSystemMessage(`**Thread suspended!** This thread will act as closed until unsuspended with \`!unsuspend\``); }); diff --git a/src/utils.js b/src/utils.js index 2a3ce6b..5fd6a0b 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,7 @@ const Eris = require('eris'); const bot = require('./bot'); const moment = require('moment'); +const humanizeDuration = require('humanize-duration'); const publicIp = require('public-ip'); const attachments = require('./data/attachments'); const config = require('./config'); @@ -293,6 +294,8 @@ function isSnowflake(str) { return snowflakeRegex.test(str); } +const humanizeDelay = (delay, opts = {}) => humanizeDuration(delay, Object.assign({conjunction: ' and '}, opts)); + module.exports = { BotError, @@ -324,4 +327,6 @@ module.exports = { setDataModelProps, isSnowflake, + + humanizeDelay, };