slash-create
Version:
Create and sync Discord slash commands!
297 lines (296 loc) • 12.8 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();
/** @private */
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;
if (opts.handler)
this.handler = opts.handler;
this.requiredPermissions = opts.requiredPermissions;
this.forcePermissions = typeof opts.forcePermissions === 'boolean' ? opts.forcePermissions : false;
this.nsfw = typeof opts.nsfw === 'boolean' ? opts.nsfw : false;
this.throttling = opts.throttling;
this.unknown = opts.unknown || false;
this.deferEphemeral = opts.deferEphemeral || false;
this.contexts = opts.contexts || [];
this.integrationTypes = opts.integrationTypes || [constants_1.ApplicationIntegrationType.GUILD_INSTALL];
this.dmPermission =
typeof opts.dmPermission === 'boolean'
? opts.dmPermission
: this.contexts.length !== 0
? this.contexts.includes(constants_1.InteractionContextType.BOT_DM)
: true;
}
/**
* The command object serialized into JSON.
* @param global Whether the command is global or not.
*/
toCommandJSON(global = true) {
return {
default_member_permissions: this.requiredPermissions
? new permissions_1.Permissions(this.requiredPermissions).valueOf().toString()
: null,
type: this.type,
name: this.commandName,
name_localizations: this.nameLocalizations || null,
description: this.description || '',
description_localizations: this.descriptionLocalizations || null,
...(global
? {
dm_permission: this.dmPermission,
contexts: this.contexts.length !== 0 ? this.contexts : null,
integration_types: this.integrationTypes
}
: {}),
nsfw: this.nsfw,
...(this.type === constants_1.ApplicationCommandType.CHAT_INPUT
? {
...(this.options
? {
options: this.options.map((o) => ({
...o,
name_localizations: o.name_localizations || null,
description_localizations: o.description_localizations || null
}))
}
: {})
}
: {}),
...(this.type === constants_1.ApplicationCommandType.ENTRY_POINT
? {
handler: this.handler
}
: {})
};
}
/**
* Get a string that mentions the command. Retuens null if the ID is not collected.
* @param subcommands The subcommands to include in the mention.
* @param guild The guild to fetch the ID from.
*/
getMention(subcommands, guild) {
const id = this.ids.get(guild || 'global');
if (!id)
return null;
return `</${this.commandName}${subcommands ? ` ${subcommands}` : ''}:${id}>`;
}
/**
* 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 (0, 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({ content: data.response, ephemeral: true });
return ctx.send({
content: `You do not have permission to use the \`${this.commandName}\` command.`,
ephemeral: true
});
}
case 'throttling': {
return ctx.send({
content: `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({ content: '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() { }
/**
* Called in order to throttle command usages before running.
* @param ctx The context being throttled
*/
async throttle(ctx) {
if (!this.throttling)
return null;
const userID = ctx.user.id;
let throttle = this._throttles.get(userID);
if (!throttle || throttle.start + this.throttling.duration * 1000 - Date.now() < 0) {
if (throttle)
clearTimeout(throttle.timeout);
throttle = {
start: Date.now(),
usages: 0,
timeout: setTimeout(() => this._throttles.delete(userID), this.throttling.duration * 1000)
};
this._throttles.set(userID, throttle);
}
// Return throttle result if the user has been throttled
if (throttle.usages + 1 > this.throttling.usages) {
const retryAfter = (throttle.start + this.throttling.duration * 1000 - Date.now()) / 1000;
return { retryAfter };
}
throttle.usages++;
return null;
}
/** 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}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32}$/u.test(opts.name))
throw new RangeError("Command name must be between 1-32 characters, matching this regex: /^[-_'\\p{L}\\p{N}\\p{sc=Deva}\\p{sc=Thai}]{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.');
(0, 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;