slash-create-modify
Version:
Create and sync Discord slash commands!
282 lines (281 loc) • 12.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Command = exports.SlashCommand = void 0;
const constants_1 = require("./constants");
const util_1 = require("./util");
const permissions_1 = require("./structures/permissions");
/** Represents a Discord slash command. */
class SlashCommand {
/**
* @param creator The instantiating creator.
* @param opts The options for the command.
*/
constructor(creator, opts) {
/**
* A map of command IDs with its guild ID (or 'global' for global commands), used for syncing command permissions.
* This will populate when syncing or collecting with {@link SlashCreator#collectCommandIDs}.
*/
this.ids = new Map();
/** Current throttle objects for the command, mapped by user ID. */
this._throttles = new Map();
if (this.constructor.name === 'SlashCommand')
throw new Error('The base SlashCommand cannot be instantiated.');
this.creator = creator;
if (!opts.unknown)
SlashCommand.validateOptions(opts);
this.type = opts.type || constants_1.ApplicationCommandType.CHAT_INPUT;
this.commandName = opts.name;
if (opts.nameLocalizations)
this.nameLocalizations = opts.nameLocalizations;
if (opts.description)
this.description = opts.description;
if (opts.descriptionLocalizations)
this.descriptionLocalizations = opts.descriptionLocalizations;
this.options = opts.options;
if (opts.guildIDs)
this.guildIDs = typeof opts.guildIDs == 'string' ? [opts.guildIDs] : opts.guildIDs;
this.requiredPermissions = opts.requiredPermissions;
this.forcePermissions = typeof opts.forcePermissions === 'boolean' ? opts.forcePermissions : false;
this.throttling = opts.throttling;
this.unknown = opts.unknown || false;
this.deferEphemeral = opts.deferEphemeral || false;
this.defaultPermission = typeof opts.defaultPermission === 'boolean' ? opts.defaultPermission : true;
this.dmPermission = typeof opts.dmPermission === 'boolean' ? opts.dmPermission : true;
if (opts.permissions)
this.permissions = opts.permissions;
}
/**
* The JSON for using commands in Discord's API.
* @private
* @deprecated Use {@link SlashCommand#toCommandJSON} instead.
*/
get commandJSON() {
return this.type === constants_1.ApplicationCommandType.CHAT_INPUT
? {
name: this.commandName,
...(this.nameLocalizations ? { name_localizations: this.nameLocalizations } : {}),
description: this.description,
...(this.descriptionLocalizations ? { description_localizations: this.descriptionLocalizations } : {}),
default_permission: this.defaultPermission,
type: constants_1.ApplicationCommandType.CHAT_INPUT,
...(this.options ? { options: this.options } : {})
}
: {
name: this.commandName,
...(this.nameLocalizations ? { name_localizations: this.nameLocalizations } : {}),
description: '',
type: this.type,
default_permission: this.defaultPermission
};
}
/**
* The command object serialized into JSON.
* @param global Whether the command is global or not.
*/
toCommandJSON(global = true) {
const hasAnyLocalizations = !!this.nameLocalizations || !!this.descriptionLocalizations;
return {
default_permission: this.defaultPermission,
default_member_permissions: this.requiredPermissions
? new permissions_1.Permissions(this.requiredPermissions).valueOf().toString()
: null,
type: this.type,
name: this.commandName,
...(hasAnyLocalizations ? { name_localizations: this.nameLocalizations || null } : {}),
description: this.description || '',
...(hasAnyLocalizations ? { description_localizations: this.descriptionLocalizations || null } : {}),
...(global ? { dm_permission: this.dmPermission } : {}),
...(this.type === constants_1.ApplicationCommandType.CHAT_INPUT
? {
...(this.options ? { options: this.options } : {})
}
: {})
};
}
/**
* The internal key name for the command.
* @private
*/
get keyName() {
const prefix = this.guildIDs ? this.guildIDs.join(',') : 'global';
return `${this.type}:${prefix}:${this.commandName}`;
}
/** The client passed from the creator */
get client() {
return this.creator.client;
}
/**
* Checks whether the context member has permission to use the command.
* @param ctx The triggering context
* @return {boolean|string} Whether the member has permission, or an error message to respond with if they don't
*/
hasPermission(ctx) {
if (this.requiredPermissions && this.forcePermissions && ctx.member) {
const missing = ctx.member.permissions.missing(this.requiredPermissions);
if (missing.length > 0) {
if (missing.length === 1) {
return `The \`${this.commandName}\` command requires you to have the "${constants_1.PermissionNames[missing[0]] || missing[0]}" permission.`;
}
return util_1.oneLine `
The \`${this.commandName}\` command requires you to have the following permissions:
${missing.map((perm) => constants_1.PermissionNames[perm] || perm).join(', ')}
`;
}
}
return true;
}
/**
* Called when the command is prevented from running.
* @param ctx Command context the command is running from
* @param reason Reason that the command was blocked
* (built-in reasons are `permission`, `throttling`)
* @param data Additional data associated with the block.
* - permission: `response` ({@link string}) to send
* - throttling: `throttle` ({@link Object}), `remaining` ({@link number}) time in seconds
*/
onBlock(ctx, reason, data) {
switch (reason) {
case 'permission': {
if (data.response)
return ctx.send(data.response, { ephemeral: true });
return ctx.send(`You do not have permission to use the \`${this.commandName}\` command.`, { ephemeral: true });
}
case 'throttling': {
return ctx.send(`You may not use the \`${this.commandName}\` command again for another ${data.remaining.toFixed(1)} seconds.`, { ephemeral: true });
}
default:
return null;
}
}
/**
* Called when the command produces an error while running.
* @param err Error that was thrown
* @param ctx Command context the command is running from
*/
onError(err, ctx) {
if (!ctx.expired && !ctx.initiallyResponded)
return ctx.send('An error occurred while running the command.', { ephemeral: true });
}
/**
* Called when the command's localization is requesting to be updated.
*/
onLocaleUpdate() { }
/**
* Called when the command is being unloaded.
*/
onUnload() { }
/**
* Creates/obtains the throttle object for a user, if necessary.
* @param userID ID of the user to throttle for
* @private
*/
throttle(userID) {
if (!this.throttling)
return null;
let throttle = this._throttles.get(userID);
if (!throttle) {
throttle = {
start: Date.now(),
usages: 0,
timeout: setTimeout(() => {
this._throttles.delete(userID);
}, this.throttling.duration * 1000)
};
this._throttles.set(userID, throttle);
}
return throttle;
}
/** Reloads the command. */
reload() {
if (!this.filePath)
throw new Error('Cannot reload a command without a file path defined!');
if (require.cache[this.filePath])
delete require.cache[this.filePath];
const newCommand = require(this.filePath);
this.creator.reregisterCommand(newCommand, this);
}
/** Unloads the command. */
unload() {
if (this.filePath && require.cache[this.filePath])
delete require.cache[this.filePath];
this.creator.unregisterCommand(this);
}
/**
* Runs the command.
* @param ctx The context of the interaction
*/
async run(ctx) {
throw new Error(`${this.constructor.name} doesn't have a run() method.`);
}
/**
* Runs an autocomplete function.
* @param ctx The context of the interaction
*/
async autocomplete(ctx) {
throw new Error(`${this.constructor.name} doesn't have a autocomplete() method.`);
}
/**
* Finalizes the return output
* @param response The response from the command
* @param ctx The context of the interaction
* @private
*/
finalize(response, ctx) {
if (!response && !ctx.initiallyResponded)
return;
if (typeof response === 'string' || (response && response.constructor && response.constructor.name === 'Object'))
return ctx.send(response);
}
/**
* Validates {@link SlashCommandOptions}.
* @private
*/
static validateOptions(opts) {
if (typeof opts.name !== 'string')
throw new TypeError('Command name must be a string.');
if (!opts.type || opts.type === constants_1.ApplicationCommandType.CHAT_INPUT) {
if (opts.name !== opts.name.toLowerCase())
throw new Error('Command name must be lowercase.');
if (!/^[\p{L}_\d-]{1,32}$/u.test(opts.name))
throw new RangeError('Command name must be under 32 characters, matching this regex: /^[\\w-]{1,32}$/');
if (typeof opts.description !== 'string')
throw new TypeError('Command description must be a string.');
if (opts.description.length < 1 || opts.description.length > 100)
throw new RangeError('Command description must be under 100 characters.');
if (opts.options) {
if (!Array.isArray(opts.options))
throw new TypeError('Command options must be an array of options.');
if (opts.options.length > 25)
throw new RangeError('Command options cannot exceed 25 options.');
util_1.validateOptions(opts.options);
}
}
else {
if (opts.name.length < 1 || opts.name.length > 32)
throw new RangeError('Command names must be between 1-32 characters.');
}
if (opts.requiredPermissions) {
if (!Array.isArray(opts.requiredPermissions))
throw new TypeError('Command required permissions must be an Array of permission key strings.');
for (const perm of opts.requiredPermissions)
if (!permissions_1.Permissions.FLAGS[perm])
throw new RangeError(`Invalid command required permission: ${perm}`);
}
if (opts.throttling) {
if (typeof opts.throttling !== 'object')
throw new TypeError('Command throttling must be an Object.');
if (typeof opts.throttling.usages !== 'number' || isNaN(opts.throttling.usages)) {
throw new TypeError('Command throttling usages must be a number.');
}
if (opts.throttling.usages < 1)
throw new RangeError('Command throttling usages must be at least 1.');
if (typeof opts.throttling.duration !== 'number' || isNaN(opts.throttling.duration)) {
throw new TypeError('Command throttling duration must be a number.');
}
if (opts.throttling.duration < 1)
throw new RangeError('Command throttling duration must be at least 1.');
}
}
}
exports.SlashCommand = SlashCommand;
exports.Command = SlashCommand;