const {Client, TextChannel} = require('discord.js');
/**
* Options for a command
* @typedef {Object} CommandOptions
* @property {number} [minArgs=0] Minimum arguments required to trigger the event
* @property {number} [maxArgs=-1] Maximum arguments allowed to trigger the event
* @property {boolean} [displayInHelp=true] If false, the command will not be displayed in the help
* @property {string} [helpMessage="No help available"] Contains the help message displayed when user uses help command
* @property {UsageMessage} [usageMessage=""] Contains the message displayed when the command is badly used.
* @property {PermissionResolvable} [requiredPermission=0] Store the permission required to use the command (Will be ignored in DM/Group DM)
* @property {boolean} [dmAllowed=false] If false, the event isn't triggered when the command is sent by PM
*/
const DefaultCommandOptions = {
minArgs: 0,
maxArgs: -1,
displayInHelp: true,
helpMessage: 'No help available',
usageMessage: '',
requiredPermission: 0,
dmAllowed: false,
};
/**
* The callback function called when a valid command is typed
* @typedef {function} CommandCallback
* @param {Message} [message] Discord.js Message sent by user
* @param {string} [commandName] Command name, without the prefix
* @param {string[]} [args] Arguments passed to the command, sliced following spaces
*/
/**
* The usage message. It's a string, but you can type %p, which will be replaced by the prefix, %c, replaced by the command name (without prefix), and %f, replaced by the full command (with prefix)
* @typedef {string} UsageMessage
*/
/**
* Represents a bot which supports commands.
* {@link https://discord.js.org/#/docs/main/stable/class/Client | Discord.js Client class documentation.}
* @extends Client
*/
class CommandClient extends Client {
/**
* Class constructor
* @param {string} [prefix='!'] Prefix for commands.
* @param {ClientOptions} [options={}] Options to be passed to the Client.
* @constructor
*/
constructor(prefix = '!', options = {}) {
super(options);
/**
* Prefix of the command
* @type {string}
*/
this.prefix = prefix;
/**
* Set if the help is enabled
* @type {boolean}
*/
this.enableHelp = true;
/**
* All registered commands
* @type {Object.<string, CommandInfos>}
* @private
*/
this._registeredCommands = [];
/**
* Message displayed when usage doesn't have enough privileges to run command.
* @type {string}
* @default "You aren't allowed to run this command."
*/
this.commandNotAllowedMessage = "You aren't allowed to run this command.";
this.on("message", (message) => {
if(message.content.startsWith(prefix)) {
let args = message.content.substr(this.prefix.length).split(' ');
const commandName = args[0];
args.shift();
if(typeof this._registeredCommands[commandName] !== 'undefined') {
const commandData = this._registeredCommands[commandName];
const commandOptions = commandData.options;
const isDm = (message.channel.type === 'dm' || message.channel.type === 'group');
if(isDm && !commandOptions.dmAllowed)
return;
if(args.length < commandOptions.minArgs) {
this._displayUsageMessage(message, commandData);
return;
}
if(commandOptions.maxArgs >= 0 && args.length > commandOptions.maxArgs) {
this._displayUsageMessage(message, commandData);
return;
}
if(!isDm && commandOptions.requiredPermission !== 0 && (!(message.channel instanceof TextChannel) || !message.channel.permissionsFor(message.author).has(commandOptions.requiredPermission))) {
this._replyMessage(message, this.commandNotAllowedMessage);
return;
}
commandData.callback(message, commandName, args);
}
}
});
this.registerCommand("help", (message) => {
if(this.enableHelp) {
const dm = (message.channel.type === 'dm' || message.channel.type === 'group');
if (dm || !(message.channel instanceof TextChannel) || message.channel.permissionsFor(this.user).has("SEND_MESSAGES")) {
let help = '';
for(let key in this._registeredCommands) {
if(this._registeredCommands.hasOwnProperty(key)) {
let commandData = this._registeredCommands[key];
if (commandData.options.displayInHelp) {
if (!dm || commandData.options.dmAllowed) {
help += `\`${this.prefix}${commandData.name}\`: ${commandData.options.helpMessage}\n`;
}
}
}
}
if(help !== '') {
message.channel.send(help);
}
}
}
}, {
helpMessage: "List all available commands and their usages",
dmAllowed: true
})
}
/**
* Register a new command
* @param {string} command Command without the prefix
* @param {CommandCallback} callback Callback function called when the command is triggered
* @param {CommandOptions} [options={}] Options evaluated when the command is triggered
*/
registerCommand(command, callback, options = {}) {
let o = {};
Object.assign(o, DefaultCommandOptions);
Object.assign(o, options);
/**
* Stores the informations about the command
* @typedef {{name: string, callback: CommandCallback, options: CommandOptions}} CommandInfos
*/
const commandInfos = {
name: command,
callback: callback,
options: o,
};
this._registeredCommands[command] = commandInfos;
}
/**
* Unregisters a command if registered
* @param {string} command Command to unregister
*/
unregisterCommand(command) {
if(typeof this._registeredCommands[command] !== 'undefined') {
delete this._registeredCommands[command];
}
}
/**
* Edit command datas (callback, and options, if specified)
* @param {string} command The command to edit
* @param {CommandCallback} callback The new callback function
* @param {CommandOptions} [options={}] The options to edit (if an existing option isn't set, its value will be unchanged)
*/
editCommandData(command,callback,options = {}) {
if(typeof this._registeredCommands[command] !== 'undefined') {
this._registeredCommands[command].callback = callback;
Object.assign(this._registeredCommands[command].options, options);
}
}
/**
* Edit command options
* @param {string} command The command to edit
* @param {CommandOptions} options The options to edit (if an existing option isn't set, its value will be unchanged)
*/
editCommandOptions(command, options) {
if(typeof this._registeredCommands[command] !== 'undefined') {
Object.assign(this._registeredCommands[command].options, options);
}
}
/**
* Display the usage message if existing.
* @param {Message} message the original Discord message
* @param {CommandInfos} commandInfos
* @private
*/
_displayUsageMessage(message, commandInfos) {
const options = commandInfos.options;
if(options.usageMessage !== '') {
let usageMessage = options.usageMessage;
usageMessage = usageMessage.replace("%p", this.prefix);
usageMessage = usageMessage.replace("%f", this.prefix + commandInfos.name);
usageMessage = usageMessage.replace("%c", commandInfos.name);
this._replyMessage(message, "Usage: `" + usageMessage + "`")
}
}
/**
* Used to reply to a message, an check to avoid errors
* @param {Message} originalMessage Original message posted by user
* @param {string} messageToReply Content of the reply
* @private
*/
_replyMessage(originalMessage, messageToReply) {
if(messageToReply !== '' && (originalMessage.channel.type === 'dm' || originalMessage.channel.type === 'group' || !(originalMessage.channel instanceof TextChannel) || originalMessage.channel.permissionsFor(this.user).has("SEND_MESSAGES"))) {
originalMessage.reply(messageToReply);
}
}
}
exports.CommandClient = CommandClient;