// Copyright 2018-2020 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const fs = require('fs');
const MessageMaker = require('./lib/MessageMaker.js');
require('./subModule.js')
.extend(CmdScheduling); // Extends the SubModule class.
/**
* @classdesc Provides interface for scheduling a specific time or interval for
* a command to be run.
* @class
* @augments SubModule
* @listens Command#schedule
* @listens Command#sch
* @listens Command#sched
* @listens Command#scheduled
*/
function CmdScheduling() {
const self = this;
/** @inheritdoc */
this.myName = 'CmdScheduling';
/** @inheritdoc */
this.initialize = function() {
const adminOnlyOpts = new self.command.CommandSetting({
validOnlyInGuild: true,
defaultDisabled: true,
permissions: self.Discord.PermissionsBitField.Flags.ManageRoles |
self.Discord.PermissionsBitField.Flags.ManageGuild |
self.Discord.PermissionsBitField.Flags.BanMembers,
});
self.command.on(
new self.command.SingleCommand(
['schedule', 'sch', 'sched', 'scheduled'], commandSchedule,
adminOnlyOpts));
const now = Date.now();
self.client.guilds.cache.forEach((g) => {
self.common.readFile(
`${self.common.guildSaveDir}${g.id}${saveSubDir}`, (err, data) => {
if (err && err.code == 'ENOENT') return;
if (err) {
self.warn('Failed to load scheduled command: ' + g.id);
return;
}
try {
const parsed = JSON.parse(data);
if (!parsed && parsed.length !== 0) {
self.warn('Failed to parse scheduled commands: ' + g.id);
return;
}
if (!schedules[g.id]) schedules[g.id] = [];
for (let i = 0; i < parsed.length; i++) {
if (parsed[i].bot != self.client.user.id) continue;
if (parsed[i].time < now) {
while (parsed[i].repeatDelay > 0 &&
parsed[i].time < now - parsed[i].repeatDelay) {
parsed[i].time += parsed[i].repeatDelay;
}
}
registerScheduledCommand(new ScheduledCommand(parsed[i]), g.id);
}
} catch (err) {
self.error('Failed to parse data for guild commands: ' + g.id);
console.error(err);
}
});
});
longInterval = setInterval(reScheduleCommands, maxTimeout);
};
/**
* @inheritdoc
* @fires CmdScheduling#shutdown
* */
this.shutdown = function() {
self.command.deleteEvent('schedule');
if (longInterval) clearInterval(longInterval);
for (const i in schedules) {
if (!schedules[i] || !schedules[i].length) continue;
schedules[i] = schedules[i].filter((el) => el.cancel(false) && false);
}
fireEvent('shutdown');
listeners = {};
};
/**
* @override
* @inheritdoc
*/
this.save = function(opt) {
self.client.guilds.cache.forEach((g) => {
if (!schedulesUpdated[g.id]) return;
delete schedulesUpdated[g.id];
if (!schedules[g.id]) schedules[g.id] = [];
schedules[g.id] = schedules[g.id].filter((el) => !el.complete);
const data = schedules[g.id].map((el) => el.toJSON());
writeSaveData(g.id, data, opt);
});
};
/**
* Write save data for a guild.
*
* @private
*
* @param {string|number} i The guild ID.
* @param {object} data The data to write.
* @param {string} [opt='sync'] See {@link save}.
*/
function writeSaveData(i, data, opt) {
const dir = `${self.common.guildSaveDir}${i}`;
const filename = `${dir}${saveSubDir}`;
if (opt === 'async') {
self.common.readAndParse(filename, (err, parsed) => {
if (!err && parsed && parsed.length > 0) {
data =
parsed.filter((el) => el.bot != self.client.user.id).concat(data);
}
const finalData = JSON.stringify(data);
self.common.mkAndWrite(filename, dir, finalData, (err) => {
if (err) {
self.error('Failed to write file: ' + filename);
console.error(err);
}
});
});
} else {
try {
const rec = fs.readFileSync(filename);
const parsed = JSON.parse(rec);
if (parsed && parsed.length > 0) {
data =
parsed.filter((el) => el.bot != self.client.user.id).concat(data);
}
} catch (err) {
// No data exists.
}
self.common.mkAndWriteSync(filename, dir, JSON.stringify(data));
}
}
/**
* Interval that runs every maxTimeout milliseconds in order to re-schedule
* commands that were beyond the max timeout duration.
*
* @private
* @type {Interval}
*/
let longInterval;
/**
* The maximum amount of time to set a Timeout for. The JS limit is 24 days
* (iirc), after which, Timeouts do not work properly.
*
* @private
* @constant
* @default 14 Days
* @type {number}
*/
const maxTimeout = 14 * 24 * 60 * 60 * 1000;
/**
* The filename in the guild directory to save the scheduled commands.
*
* @private
* @constant
* @default
* @type {string}
*/
const saveSubDir = '/scheduledCmds.json';
/**
* The possible characters that can make up an ID of a scheduled command.
*
* @private
* @constant
* @default
* @type {string}
*/
const idChars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
/**
* The color to use for embeds sent from this submodule.
*
* @private
* @constant
* @default
* @type {number[]}
*/
const embedColor = [50, 255, 255];
/**
* Minimum allowable amount of time in milliseconds from when the scheduled
* command is registered to when it runs.
*
* @public
* @constant
* @default 10 Seconds
* @type {number}
*/
this.minDelay = 10000;
/**
* Minimum allowable amount of time in milliseconds from when the scheduled
* command is run to when it run may run again.
*
* @public
* @constant
* @default 30 Seconds
* @type {number}
*/
this.minRepeatDelay = 30000;
/**
* Currently registered event listeners, mapped by event name.
*
* @private
* @type {object.<Array.<Function>>}
*/
let listeners = {};
/**
* @description All of the guilds that have updated their schedules since last
* save.
* @private
* @type {object.<boolean>}
* @default
*/
const schedulesUpdated = {};
/**
* All of the currently loaded commands to run. Mapped by Guild ID, then
* sorted arrays by time to run next command.
*
* @private
* @type {object.<Array.<CmdScheduling.ScheduledCommand>>}
*/
const schedules = {};
/**
* @classdesc Stores information about a specific command that is scheduled.
* @class
*
* @public
* @param {string|object} cmd The command to run, or an object instance of
* this class (exported using toJSON, then parsed into an object).
* @param {string|number|Discord~TextChannel} channel The channel or channel
* id of where to run the command.
* @param {string|number|Discord~Message} message The message or message id
* that created this scheduled command.
* @param {number} time The unix timestamp at which to run the command.
* @param {?number} repeatDelay The delay in milliseconds at which to run the
* command again, or null if it does not repeat.
*
* @property {string} cmd The command to run.
* @property {number|string} bot The id of the bot instantiating this command.
* @property {Discord~TextChannel} channel The channel or channel id of where
* to run the command.
* @property {string|number} channelId The id of the channel where the message
* was sent.
* @property {?Discord~Message} message The message that created this
* scheduled command, or null if the message was deleted.
* @property {string|number} messageId The id of the message sent.
* @property {number} time The unix timestamp at which to run the command.
* @property {number} [repeatDelay=0] The delay in milliseconds at which to
* run the command again. 0 to not repeat.
* @property {string} id Random base 36, 3-character long id of this command.
* @property {boolean} complete True if the command has been run, and will not
* run again.
* @property {Timeout} timeout The current timeout registered to run the
* command.
* @property {Discord~GuildMember} member The author of this ScheduledCommand.
* @property {string|number} memberId The id of the member.
*/
function ScheduledCommand(cmd, channel, message, time, repeatDelay = 0) {
const myself = this;
if (typeof cmd === 'object') {
channel = cmd.channel;
message = cmd.message;
time = cmd.time;
repeatDelay = cmd.repeatDelay;
this.id = cmd.id;
this.member = cmd.member;
cmd = cmd.cmd;
} else {
this.member = message.member;
this.id = '';
}
if (!this.id || this.id.length < 3) {
this.id = '';
for (let i = 0; i < 3; i++) {
this.id += idChars.charAt(Math.floor(Math.random() * idChars.length));
}
}
this.cmd = cmd;
this.channel = channel;
this.channelId = typeof channel === 'object' ? channel.id : channel;
this.message = message;
this.messageId = typeof message === 'object' ? message.id : message;
this.time = time;
this.repeatDelay = repeatDelay;
this.memberId =
typeof this.member === 'object' ? this.member.id : this.member;
this.bot = self.client.user.id;
this.complete = false;
let runAfterRefs = false;
let isFetching = false;
/**
* Update channel and message with their associated IDs.
*
* @private
*/
function getReferences() {
if (!myself.channel || typeof myself.channel !== 'object') {
myself.channel = self.client.channels.resolve(myself.channelId);
}
if (!myself.channel || typeof myself.channel !== 'object' ||
myself.channel.deleted) {
self.debug(
'Cancelling command due to channel not existing: ' +
myself.channelId + '@' + myself.memberId + ': ' + myself.cmd);
myself.cancel();
return;
}
if (!myself.message || typeof myself.message !== 'object') {
myself.message = myself.channel.messages.resolve(myself.messageId);
if (!myself.message && !isFetching) {
isFetching = true;
myself.channel.messages.fetch(myself.messageId)
.then((msg) => {
if (!msg) throw new Error();
myself.message = msg;
myself.member = msg.member;
myself.memberId = msg.member.id;
if (runAfterRefs) myself.go();
})
.catch(() => {
self.debug(
'Failed to find message: ' + myself.channelId + '@' +
myself.memberId + ' ' + myself.messageId + ': ' +
myself.cmd);
myself.message = makeMessage(
myself.memberId, myself.channel.guild.id, myself.channel.id,
myself.cmd);
myself.member = myself.message.member;
myself.memberId = myself.member.id;
if (runAfterRefs) myself.go();
});
} else if (!isFetching) {
myself.member = myself.message.member;
myself.memberId = myself.message.member.id;
if (runAfterRefs) myself.go();
}
}
if (!myself.member || typeof myself.member !== 'object') {
myself.member = myself.channel.members.get(myself.memberId);
if (!myself.member && !isFetching) {
isFetching = true;
myself.channel.guild.members.fetch(myself.memberId)
.then((m) => myself.member = m)
.catch((err) => {
self.error(
'Failed to find member with id: ' + myself.memberId +
' in guild: ' + myself.channel.guild.id);
console.error(err);
});
}
}
}
/**
* Trigger the command to be run immediately. Automatically fired at the
* scheduled time. Does not cancel the normally scheduled command.
* Re-schedules the command if the command should repeat.
*
* @public
*/
this.go = function() {
if (myself.complete) {
self.error('Command triggered after being completed!', myself.id);
clearTimeout(myself.timeout);
return;
}
const now = Date.now();
runAfterRefs = false;
getReferences();
if (!myself.channel || !myself.channel.send) {
self.error(
'ScheduledCmdFailed No Channel: ' + myself.channel.id +
'@' + myself.memberId + ' ' + myself.cmd);
myself.complete = true;
clearTimeout(myself.timeout);
return;
} else if (!myself.message) {
self.error(
'ScheduledCmdWarning No Message: ' + myself.channel.guild.id + '#' +
myself.channel.id + '@' + myself.memberId + ' ' + myself.cmd);
runAfterRefs = true;
return;
} else if (!myself.message.channel || !myself.message.channel.send) {
self.warn(
'ScheduledCmdWarning No Message Channel: ' +
myself.channel.guild.id + '#' + myself.channel.id + '@' +
myself.memberId + ' ' + myself.cmd);
myself.message.channel = myself.channel;
}
if (!myself.message.guild.members ||
typeof myself.message.guild.members.resolve !== 'function') {
self.error(
'ScheduledCmdFailed No Members Channel: ' +
myself.channel.guild.id + '#' + myself.channel.id + '@' +
myself.memberId + ' ' + myself.cmd);
return;
} else if (!myself.message.channel.permissionsFor(self.client.user)
.has(self.Discord.PermissionsBitField.Flags.SendMessages)) {
self.error(
'ScheduledCmdWarning No perm SEND_MESSAGES: ' +
myself.channel.guild.id + '#' + myself.channel.id + '@' +
myself.memberId + ' ' + myself.cmd);
return;
} else if (!myself.message.channel.permissionsFor(self.client.user)
.has(self.Discord.PermissionsBitField.Flags.ViewChannel)) {
self.error(
'ScheduledCmdWarning No perm VIEW_CHANNEL: ' +
myself.channel.guild.id + '#' + myself.channel.id + '@' +
myself.memberId + ' ' + myself.cmd);
return;
}
myself.message.content = myself.cmd;
myself.message.fabricated = true;
myself.message.disableMention = true;
const cmd = self.command.find(myself.cmd, myself.message);
if (!cmd) {
self.error(
'Unknown ScheduledCmd: ' + myself.message.channel.id + '@' +
myself.message.author.id + ' ' + myself.cmd + ' ' +
myself.message.content);
return;
}
if (cmd.getFullName() === self.command.find('sch').getFullName()) {
self.error(
'Recursive ScheduledCmd: ' + myself.message.channel.id + '@' +
myself.message.author.id + ' ' + myself.message.content);
return;
}
self.debug(
'ScheduledCmd: ' + myself.message.channel.id + '@' +
myself.message.author.id + ' ' + myself.message.content);
try {
self.command.trigger(myself.message);
} catch (err) {
self.error(
'Failed to trigger ScheduledCmd: ' + myself.message.channel.id +
'@' + myself.message.author.id + ' ' + myself.message.content);
console.error(err);
}
// If the command was fired at the scheduled time, or if it was fired
// manually and the the scheduled time is in less than a second, then
// consider the scheduled command to have been completed.
if (myself.time - 1000 <= now) {
clearTimeout(myself.timeout);
if (myself.repeatDelay > 0) {
myself.complete = false;
myself.time += myself.repeatDelay;
sortGuildCommands(myself.message.guild.id);
myself.setTimeout();
} else {
myself.complete = true;
}
schedulesUpdated[myself.message.guild.id] = true;
}
};
/**
* Cancel this command and remove Timeout.
*
* @public
* @param {boolean} [markComplete=true] Should we mark this command as
* completed after cancelling.
*/
this.cancel = function(markComplete = true) {
clearTimeout(myself.timeout);
if (markComplete) myself.complete = true;
};
/**
* Schedule the Timeout event to call the command at the scheduled time. If
* the scheduled time to run the command is more than 2 weeks in the future,
* the command is not scheduled, and this function must be called manually
* (less than 2 weeks) before the scheduled time for the command to run.
*
* @public
*/
this.setTimeout = function() {
if (myself.complete) {
return; // Command was completed, and should no longer run.
}
if (myself.time - Date.now() <= maxTimeout) {
clearTimeout(myself.timeout);
try {
myself.timeout = setTimeout(myself.go, myself.time - Date.now());
} catch (err) {
self.error(
'ScheduledCmd Failed: ' + myself.channelId + '@' +
myself.memberId + ' ' + myself.cmd);
return;
}
self.debug(
'ScheduledCmd Scheduled: ' + myself.channelId + '@' +
myself.memberId + ' ' + myself.cmd);
}
};
/**
* Export the relevant data to recreate this object, as a JSON object.
*
* @public
* @returns {object} JSON formatted object.
*/
this.toJSON = function() {
return {
bot: self.client.user.id,
cmd: myself.cmd,
time: myself.time,
repeatDelay: myself.repeatDelay,
id: myself.id,
channel: myself.channelId,
message: myself.messageId,
member: myself.memberId,
};
};
getReferences();
setTimeout(() => this.setTimeout());
}
this.ScheduledCommand = ScheduledCommand;
/**
* Register a created {@link CmdScheduling.ScheduledCommand}.
*
* @private
* @fires CmdScheduling#commandRegistered
*
* @param {CmdScheduling.ScheduledCommand} sCmd The ScheduledCommand object to
* register.
* @param {string} [gId] Guild ID if message has not yet been found, or to
* override the ID found in the given object.
* @returns {boolean} True if succeeded, False if too close to existing
* command.
*/
function registerScheduledCommand(sCmd, gId) {
if (!gId) gId = sCmd.message.guild.id;
if (!schedules[gId]) {
schedules[gId] = [sCmd];
} else {
for (let i = 0; i < schedules[gId].length; i++) {
if (Math.abs(schedules[gId][i].time - sCmd.time) < 5000) {
sCmd.cancel();
return false;
}
}
schedules[gId].push(sCmd);
}
if (sCmd.message) {
schedulesUpdated[gId] = true;
fireEvent('commandRegistered', sCmd, sCmd.message.guild.id);
}
return true;
}
/**
* Register a created {@link CmdScheduling.ScheduledCommand}.
*
* @public
* @see {@link CmdScheduling~registerScheduledCommand}
*/
this.registerScheduledCommand = registerScheduledCommand;
/**
* Allow user to schedule command to be run, or view currently scheduled
* commands.
*
* @private
* @type {commandHandler}
* @param {Discord~Message} msg Message that triggered command.
* @listens Command#schedule
*/
function commandSchedule(msg) {
if (!msg.text || !msg.text.trim()) {
replyWithSchedule(msg);
return;
} else if (msg.text.match(/(cancel|remove|delete)/)) {
cancelAndReply(msg);
return;
}
const splitCmd = msg.text.trim().split(msg.prefix);
if (!splitCmd || splitCmd.length < 2 || splitCmd[0].trim().length == 0) {
self.common.reply(
msg,
'Oops! Please ensure you have formatted that correctly.\nI wasn\'t' +
' able to understand it properly.');
return;
}
let delay = splitCmd.splice(0, 1)[0];
const cmd = msg.prefix + splitCmd.join(msg.prefix);
let repeat = 0;
const invalid = self.command.validate(cmd.split(/\s/)[0], msg);
if (invalid) {
self.common.reply(
msg, 'That command doesn\'t seem to be a usable command.\n' + cmd,
invalid);
return;
}
if (self.command.find(splitCmd[0]).getFullName() ===
self.command.find('sch').getFullName()) {
self.common.reply(msg, 'Commands may not be recursive.', invalid);
return;
}
if (delay.match(/every|repeat/)) {
const splitTimes = delay.match(/^(.*?)(every|repeat)(.*)$/);
delay = splitTimes[1];
repeat = splitTimes[3];
}
delay = stringToMilliseconds(delay);
/* if (delay < self.minDelay) {
self.common.reply(msg, 'Sorry, but delays must be more than 10 seconds.');
return;
} */
repeat = stringToMilliseconds(repeat);
if (repeat && repeat < self.minRepeatDelay) {
self.common.reply(
msg, 'Sorry, but repeat delays must be more than 30 seconds.');
return;
}
const newCmd =
new ScheduledCommand(cmd, msg.channel, msg, delay + Date.now(), repeat);
if (!registerScheduledCommand(newCmd)) {
self.common.reply(
msg, 'Sorry, but commands must be separated by at least 5 seconds.');
return;
}
const embed = new self.Discord.EmbedBuilder();
embed.setTitle('Created Scheduled Command (' + newCmd.id + ')');
embed.setColor(embedColor);
let desc = 'Runs in ' + formatDelay(delay);
if (repeat) {
desc += '\nRepeats every ' + formatDelay(repeat);
}
embed.setDescription(desc);
embed.addFields([
{name: 'To cancel:', value: `\`${msg.prefix}sch cancel ${newCmd.id}\``},
]);
embed.setFooter({text: cmd});
msg.channel.send({content: self.common.mention(msg), embeds: [embed]});
}
/**
* Sort all scheduled commands in a guild by the next time they will run.
*
* @private
* @param {string|number} id The guild id of which to sort the commands.
*/
function sortGuildCommands(id) {
const c = schedules[id];
if (!c) return;
let unsorted = true;
while (unsorted) {
unsorted = false;
let spliced;
for (let i = 1; i < c.length; i++) {
if (!spliced && c[i - 1].time > c[0].time) {
spliced = c.splice(i - 1, 1)[0];
if (c[c.length - 1].time < spliced.time) {
c.push(spliced);
spliced = null;
i--;
}
} else if (spliced && c[i].time < spliced.time) {
c.splice(i + 1, 0, spliced);
unsorted = true;
break;
}
}
}
}
/**
* Given a user-inputted string, convert to a number of milliseconds. Input
* can be on most common time units up to a week.
*
* @private
*
* @param {string} str The input string to parse.
* @returns {number} Number of milliseconds parsed from string.
*/
function stringToMilliseconds(str) {
let sum = 0;
str = (str + '')
.replace(/\b(and|repeat|every|after|in)\b/g, '')
.trim()
.toLowerCase();
const reg = /([0-9.]+)([^a-z]*)([a-z]*)/g;
let res;
while ((res = reg.exec(str)) !== null) {
sum += numberToUnit(res[1], res[3]);
}
if (!sum && str) {
sum = numberToUnit(1, str);
}
/**
* Convert a number and a unit to the corresponding number of milliseconds.
*
* @private
* @param {number} num The number associated with the unit.
* @param {string} unit The current unit associated with the num.
* @returns {number} The given number in milliseconds.
*/
function numberToUnit(num, unit) {
switch (unit) {
case 's':
case 'sec':
case 'second':
case 'seconds':
return num * 1000;
case 'm':
case 'min':
case 'minute':
case 'minutes':
return num * 60 * 1000;
case 'h':
case 'hr':
case 'hour':
case 'hours':
return num * 60 * 60 * 1000;
case 'd':
case 'dy':
case 'day':
case 'days':
return num * 24 * 60 * 60 * 1000;
case 'w':
case 'wk':
case 'week':
case 'weeks':
return num * 7 * 24 * 60 * 60 * 1000;
default:
return 0;
}
}
return sum;
}
/**
* Returns an array of references to scheduled commands in a guild.
*
* @public
*
* @param {string|number} gId The guild id of which to get the commands.
* @returns {null|CmdScheduling.ScheduledCommand[]} Null if none, or the array
* of ScheduledCommands.
*/
function getScheduledCommandsInGuild(gId) {
let list = schedules[gId];
if (!list) return null;
list = list.filter((el) => !el.complete);
if (!list || list.length == 0) return null;
return list;
}
this.getScheduledCommandsInGuild = getScheduledCommandsInGuild;
/**
* Find all scheduled commands for a certain guild, and reply to the message
* with the list of commands.
*
* @private
* @param {Discord~Message} msg The message to reply to.
*/
function replyWithSchedule(msg) {
const embed = new self.Discord.EmbedBuilder();
embed.setTitle('Scheduled Commands');
embed.setColor(embedColor);
let list = getScheduledCommandsInGuild(msg.guild.id);
if (!list) {
embed.setDescription('No commands are scheduled.');
} else {
const n = Date.now();
list = list.map((el) => {
return '**' + el.id + '**: In ' + formatDelay(el.time - n) +
(el.repeatDelay ?
(', repeats every ' + formatDelay(el.repeatDelay)) :
'') +
(el.message && ' by <@' + el.message.author.id + '>: ') + el.cmd;
});
embed.setDescription(list.join('\n'));
}
if (msg.author.id == self.common.spikeyId) {
const keys = Object.keys(schedules);
let total = 0;
keys.forEach((k) => {
total += Object.keys(schedules[k]).length;
});
embed.setFooter({text: total});
}
msg.channel.send({content: self.common.mention(msg), embeds: [embed]})
.catch((err) => {
self.error('Failed to send reply in channel: ' + msg.channel.id);
console.error(err);
});
}
/**
* Cancel a scheduled command in a guild.
*
* @private
* @fires CmdScheduling#commandCancelled
*
* @param {string|number} gId The guild id of which to cancel the command.
* @param {string|number} cmdId The ID of the command to cancel.
* @returns {?CmdScheduling.ScheduledCommand} Null if failed, or object that
* was cancelled.
*/
function cancelCmd(gId, cmdId) {
const list = schedules[gId];
if (!list || list.length == 0) return null;
if (!cmdId) return null;
cmdId = `${cmdId}`.toUpperCase();
for (let i = 0; i < list.length; i++) {
if (list[i].complete) continue;
if (list[i].id == cmdId) {
const removed = list.splice(i, 1)[0];
removed.cancel();
schedulesUpdated[gId] = true;
fireEvent(
'commandCancelled', removed.id,
removed.channel && removed.channel.guild.id);
return removed;
}
}
return null;
}
/**
* Cancel a scheduled command in a guild.
*
* @public
* @see {@link CmdScheduling~cancelCmd}
*/
this.cancelCmd = cancelCmd;
/**
* Find a scheduled command with the given ID, and remove it from commands to
* run.
*
* @private
* @param {Discord~Message} msg The message to reply to.
*/
function cancelAndReply(msg) {
const embed = new self.Discord.EmbedBuilder();
embed.setColor(embedColor);
const list = schedules[msg.guild.id];
if (!list || list.length == 0) {
embed.setTitle('Cancelling Failed');
embed.setDescription('There are no scheduled commands in this guild.');
} else {
let idSearch = msg.text.match(/(cancel|remove|delete)\W+(\w{3,})\b/);
if (!idSearch) {
embed.setTitle('Cancelling Failed');
embed.setDescription('Please specify a scheduled command ID.');
} else {
idSearch = idSearch[2];
const removed = cancelCmd(msg.guild.id, idSearch);
if (!removed) {
embed.setTitle('Cancelling Failed');
embed.setDescription(
'Unable to find scheduled command with ID: ' + idSearch);
} else {
embed.setTitle('Cancelling Succeeded');
embed.setDescription(
'Removed scheduled command ID: ' + idSearch + ', ' + removed.cmd);
}
}
}
msg.channel.send({content: self.common.mention(msg), embeds: [embed]})
.catch((err) => {
self.error('Failed to send reply in channel: ' + msg.channel.id);
console.error(err);
});
}
/**
* Reschedule all future commands that are beyond maxTimeout.
*/
function reScheduleCommands() {
for (const g in schedules) {
if (!schedules[g] || !schedules[g].length) continue;
for (let i = 0; i < schedules[g].length; i++) {
let abort = false;
for (let j = 0; j < schedules[g].length; j++) {
if (i == j) continue;
if (Math.abs(schedules[g][i].time - schedules[g][j].time) < 5000) {
abort = true;
break;
}
}
if (abort) {
schedules[g][i].cancel();
schedules[g].splice(i, 1);
schedulesUpdated[g] = true;
} else {
schedules[g][i].setTimeout();
}
}
}
}
/**
* Format a duration in milliseconds into a human readable string.
*
* @private
*
* @param {number} msecs Duration in milliseconds.
* @returns {string} Formatted string.
*/
function formatDelay(msecs) {
let output = '';
let unit = 7 * 24 * 60 * 60 * 1000;
if (msecs >= unit) {
const num = Math.floor(msecs / unit);
output += num + ' week' + (num == 1 ? '' : 's') + ', ';
msecs -= num * unit;
}
unit /= 7;
if (msecs >= unit) {
const num = Math.floor(msecs / unit);
output += num + ' day' + (num == 1 ? '' : 's') + ', ';
msecs -= num * unit;
}
unit /= 24;
if (msecs >= unit) {
const num = Math.floor(msecs / unit);
output += num + ' hour' + (num == 1 ? '' : 's') + ', ';
msecs -= num * unit;
}
unit /= 60;
if (msecs >= unit) {
const num = Math.floor(msecs / unit);
output += num + ' minute' + (num == 1 ? '' : 's') + ', ';
msecs -= num * unit;
}
unit /= 60;
if (msecs >= unit) {
const num = Math.round(msecs / unit);
output += num + ' second' + (num == 1 ? '' : 's') + '';
}
return output.replace(/,\s$/, '');
}
/**
* Register an event handler for the given name with the given handler.
*
* @public
* @param {string} name The event name to listen for.
* @param {Function} handler The function to call when the event is fired.
*/
this.on = function(name, handler) {
if (typeof handler !== 'function') {
throw (new Error('Handler must be a function.'));
}
if (!listeners[name]) listeners[name] = [];
listeners[name].push(handler);
};
/**
* Remove an event handler for the given name.
*
* @public
* @param {string} name The event name to remove the handler for.
* @param {Function} [handler] THe specific handler to remove, or null for
* all.
*/
this.removeListener = function(name, handler) {
if (!listeners[name]) return;
if (!handler) {
delete listeners[name];
} else {
for (let i = 0; i < listeners[name].length; i++) {
if (listeners[name][i] == handler) {
listeners[name].splice(i, 1);
}
}
if (listeners[name].length == 0) delete listeners[name];
}
};
/**
* @description Fires a given event with the associated data.
*
* @private
* @param {string} name The name of the event to fire.
* @param {*} data The arguments to pass into the function calls.
*/
function fireEvent(name, ...data) {
for (let i = 0; listeners[name] && i < listeners[name].length; i++) {
try {
listeners[name][i](...data);
} catch (err) {
self.error('Error in firing event: ' + name);
console.error(err);
}
}
}
/**
* Forms a Discord~Message similar object from given IDs.
*
* @private
* @param {string} uId The id of the user who wrote this message.
* @param {string} gId The id of the guild this message is in.
* @param {string} cId The id of the channel this message was 'sent' in.
* @param {string} msg The message content.
* @returns {?MessageMaker} The created message-like object, or null if
* invalid channel.
*/
function makeMessage(uId, gId, cId, msg) {
if (!cId) return null;
const message = new MessageMaker(self, uId, gId, cId, msg);
return message.guild ? message : null;
}
}
module.exports = new CmdScheduling();