@distype/cmd
Version:
A command handler for Distype.
484 lines (483 loc) • 21.1 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommandHandler = void 0;
const ChatCommand_1 = require("./commands/ChatCommand");
const MessageCommand_1 = require("./commands/MessageCommand");
const UserCommand_1 = require("./commands/UserCommand");
const Button_1 = require("./components/Button");
const ChannelSelect_1 = require("./components/ChannelSelect");
const MentionableSelect_1 = require("./components/MentionableSelect");
const RoleSelect_1 = require("./components/RoleSelect");
const StringSelect_1 = require("./components/StringSelect");
const UserSelect_1 = require("./components/UserSelect");
const Modal_1 = require("./modals/Modal");
const sanitizeCommand_1 = require("../utils/sanitizeCommand");
const DiscordTypes = __importStar(require("discord-api-types/v10"));
const distype_1 = require("distype");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const node_util_1 = require("node:util");
/**
* The command handler.
*/
class CommandHandler {
/**
* The client the command handler is bound to.
*/
client;
/**
* The system string used for logging.
*/
system = `Command Handler`;
/**
* Bound commands.
* Key is their ID.
*/
_boundCommands = new distype_1.ExtendedMap();
/**
* Bound components.
*/
_boundComponents = new Set();
/**
* Bound expires.
*/
_boundExpires = new Set();
/**
* Bound modals.
*/
_boundModals = new Set();
/**
* Error function.
*/
_error = () => { };
/**
* Middleware function.
*/
_middleware = () => true;
/**
* Create the command handler.
* @param client The Distype client to bind the command handler to.
*/
constructor(client) {
this.client = client;
this.client.gateway.on(`INTERACTION_CREATE`, ({ d }) => this._onInteraction(d));
this.client.log(`Initialized command handler`, {
level: `DEBUG`,
system: this.system,
});
}
/**
* Returns structures found in a directory and its subdirectories.
* Only loads default exports.
* @param directories The directory to search.
* @returns Found structures.
*/
async extractFromDirectories(...directories) {
const structures = [];
for (const directory of directories) {
const path = (0, node_path_1.isAbsolute)(directory)
? directory
: (0, node_path_1.resolve)(process.cwd(), directory);
const files = await (0, promises_1.readdir)(path, { withFileTypes: true });
for (const file in files) {
if (files[file].isDirectory()) {
structures.push(...(await this.extractFromDirectories((0, node_path_1.resolve)(path, files[file].name))));
continue;
}
if (!files[file].name.endsWith(`.js`))
continue;
delete require.cache[require.resolve((0, node_path_1.resolve)(path, files[file].name))];
const imported = await Promise.resolve(`${(0, node_path_1.resolve)(path, files[file].name)}`).then(s => __importStar(require(s)));
const structure = imported.default ?? imported;
if (CommandHandler.isCompatableStructure(structure))
structures.push(structure);
}
}
return structures;
}
/**
* Loads interaction structures from a directory and its subdirectories.
* Only loads default exports. Note that {@link Expire expire helpers} cannot be loaded.
* @param directories The directory to search.
*/
async loadDirectories(...directories) {
const structures = await this.extractFromDirectories(...directories);
const commands = structures.filter((structure) => CommandHandler.isCommand(structure));
const customIds = structures.filter((structure) => CommandHandler.isComponent(structure) ||
CommandHandler.isModal(structure));
await this.pushCommands(...commands);
this.bind(...customIds);
}
/**
* Pushes {@link Command commands} to the API.
* Note that guilds that already have commands published that dont have any defined locally will not be overwritten.
* @param commands Commands to load.
*/
async pushCommands(...commands) {
this.client.log(`Pushing ${commands.length} commands`, {
level: `INFO`,
system: this.system,
});
await this._pushGlobalCommands(commands);
await this._pushGuildCommands(commands);
}
/**
* Binds structures that use custom IDs.
* @param structures The structures to bind.
* @returns The command handler.
*/
bind(...structures) {
structures.forEach((structure) => {
if (CommandHandler.isComponent(structure)) {
this._boundComponents.add(structure);
}
else if (CommandHandler.isModal(structure)) {
this._boundModals.add(structure);
}
else {
structure.commandHandler = this;
this._boundExpires.add(structure);
this.bind(...structure.structures);
structure.resetTimer();
}
});
return this;
}
/**
* Unbind structures that use custom IDs.
* @param structures The structures to unbind.
* @returns The command handler.
*/
unbind(...structures) {
structures.forEach((structure) => {
if (CommandHandler.isComponent(structure)) {
this._boundComponents.delete(structure);
}
else if (CommandHandler.isModal(structure)) {
this._boundModals.delete(structure);
}
else {
this._boundExpires.delete(structure);
this.unbind(...structure.structures);
structure.clearTimer();
}
});
return this;
}
/**
* Set the error function for the command handler.
* @returns The command handler.
*/
setError(errorFunction) {
this._error = errorFunction;
return this;
}
/**
* Set the middleware function for the command handler.
* @returns The command handler.
*/
setMiddleware(middlewareFunction) {
this._middleware = middlewareFunction;
return this;
}
/**
* Checks if a structure is a {@link Command command}.
*/
static isCommand(structure) {
return (structure instanceof ChatCommand_1.ChatCommand ||
structure instanceof MessageCommand_1.MessageCommand ||
structure instanceof UserCommand_1.UserCommand);
}
/**
* Checks if a structure is a {@link Component component}.
*/
static isComponent(structure) {
return (structure instanceof Button_1.Button ||
structure instanceof ChannelSelect_1.ChannelSelect ||
structure instanceof MentionableSelect_1.MentionableSelect ||
structure instanceof RoleSelect_1.RoleSelect ||
structure instanceof StringSelect_1.StringSelect ||
structure instanceof UserSelect_1.UserSelect);
}
/**
* Checks if a structure is a {@link Modal modal}.
*/
static isModal(structure) {
return structure instanceof Modal_1.Modal;
}
/**
* Checks if a structure is compatible with the command handler.
*/
static isCompatableStructure(structure) {
return (this.isCommand(structure) ||
this.isComponent(structure) ||
this.isModal(structure));
}
/**
* Pushes global {@link Command commands} to the API.
* @param commands Commands to load.
*/
async _pushGlobalCommands(commands) {
if (!this.client.gateway.user?.id)
throw new Error(`Unable to push global commands: application ID is undefined (client.gateway.user.id)`);
const local = commands
.filter((command) => !command.getGuild())
.map((command) => (0, sanitizeCommand_1.sanitizeCommand)(command.getRaw()));
const published = await this.client.rest.getGlobalApplicationCommands(this.client.gateway.user.id, { with_localizations: true });
this.client.log(`Found ${published.length} published global commands`, {
level: `DEBUG`,
system: this.system,
});
const deletedCommands = published.filter((published) => !local.find((local) => (0, node_util_1.isDeepStrictEqual)(local, (0, sanitizeCommand_1.sanitizeCommand)(published))));
const newCommands = local.filter((local) => !published.find((published) => (0, node_util_1.isDeepStrictEqual)(local, (0, sanitizeCommand_1.sanitizeCommand)(published))));
if (deletedCommands.length)
this.client.log(`Delete (Global): ${deletedCommands.map((command) => `"${command.name}"`).join(`, `)}`, {
level: `DEBUG`,
system: this.system,
});
if (newCommands.length)
this.client.log(`New (Global): ${newCommands.map((command) => `"${command.name}"`).join(`, `)}`, {
level: `DEBUG`,
system: this.system,
});
if (deletedCommands.length === published.length) {
await this.client.rest.bulkOverwriteGlobalApplicationCommands(this.client.gateway.user.id, newCommands);
}
else {
for (const command of deletedCommands) {
await this.client.rest.deleteGlobalApplicationCommand(this.client.gateway.user.id, command.id);
}
for (const command of newCommands) {
await this.client.rest.createGlobalApplicationCommand(this.client.gateway.user.id, command);
}
}
const newPublished = newCommands.length + deletedCommands.length
? await this.client.rest.getGlobalApplicationCommands(this.client.gateway.user.id, {})
: published;
newPublished.forEach((command) => {
const foundLocal = commands.find((local) => !local.getGuild() && local.getRaw().name === command.name);
if (foundLocal)
this._boundCommands.set(command.id, foundLocal);
});
this.client.log(`Created ${newCommands.length} global commands and deleted ${deletedCommands.length} global commands (Application now owns ${newPublished.length} global commands)`, {
level: `INFO`,
system: this.system,
});
}
/**
* Pushes guild {@link Command commands} to the API.
* @param commands Commands to load.
*/
async _pushGuildCommands(commands) {
if (!this.client.gateway.user?.id)
throw new Error(`Unable to push guild commands: application ID is undefined (client.gateway.user.id)`);
const guilds = new Set(commands
.map((command) => command.getGuild())
.filter((guild) => guild));
for (const guild of guilds) {
const local = commands
.filter((command) => command.getGuild() === guild)
.map((command) => (0, sanitizeCommand_1.sanitizeGuildCommand)(command.getRaw()));
const published = await this.client.rest.getGuildApplicationCommands(this.client.gateway.user.id, guild, { with_localizations: true });
this.client.log(`Found ${published.length} published commands in guild ${guild}`, {
level: `DEBUG`,
system: this.system,
});
const deletedCommands = published.filter((published) => !local.find((local) => (0, node_util_1.isDeepStrictEqual)(local, (0, sanitizeCommand_1.sanitizeGuildCommand)(published))));
const newCommands = local.filter((local) => !published.find((published) => (0, node_util_1.isDeepStrictEqual)(local, (0, sanitizeCommand_1.sanitizeGuildCommand)(published))));
if (deletedCommands.length)
this.client.log(`Delete (${guild}): ${deletedCommands.map((command) => `"${command.name}"`).join(`, `)}`, {
level: `DEBUG`,
system: this.system,
});
if (newCommands.length)
this.client.log(`New (${guild}): ${newCommands.map((command) => `"${command.name}"`).join(`, `)}`, {
level: `DEBUG`,
system: this.system,
});
if (deletedCommands.length === published.length) {
await this.client.rest.bulkOverwriteGuildApplicationCommands(this.client.gateway.user.id, guild, newCommands);
}
else {
for (const command of deletedCommands) {
await this.client.rest.deleteGuildApplicationCommand(this.client.gateway.user.id, guild, command.id);
}
for (const command of newCommands) {
await this.client.rest.createGuildApplicationCommand(this.client.gateway.user.id, guild, command);
}
}
const newPublished = newCommands.length + deletedCommands.length
? await this.client.rest.getGuildApplicationCommands(this.client.gateway.user.id, guild, {})
: published;
newPublished.forEach((command) => {
const foundLocal = commands.find((local) => local.getRaw().name === command.name);
if (foundLocal)
this._boundCommands.set(command.id, foundLocal);
});
this.client.log(`Created ${newCommands.length} commands and deleted ${deletedCommands.length} commands in guild ${guild} (Application now owns ${newPublished.length} commands in guild ${guild})`, {
level: `INFO`,
system: this.system,
});
}
}
/**
* Callback to run when receiving an interaction.
* @param interaction The received interaction.
*/
async _onInteraction(interaction) {
let structure;
let context;
switch (interaction.type) {
case DiscordTypes.InteractionType.ApplicationCommand: {
structure = this._boundCommands.get(interaction.data.id);
switch (interaction.data.type) {
case DiscordTypes.ApplicationCommandType.ChatInput: {
if (structure)
context = new ChatCommand_1.ChatCommandContext(interaction, this);
break;
}
case DiscordTypes.ApplicationCommandType.Message: {
if (structure)
context = new MessageCommand_1.MessageCommandContext(interaction, this);
break;
}
case DiscordTypes.ApplicationCommandType.User: {
if (structure)
context = new UserCommand_1.UserCommandContext(interaction, this);
break;
}
}
break;
}
case DiscordTypes.InteractionType.MessageComponent: {
structure = Array.from(this._boundComponents).find((component) => component.getCustomId() === interaction.data.custom_id &&
component.getType() === interaction.data.component_type);
switch (interaction.data.component_type) {
case DiscordTypes.ComponentType.Button: {
if (structure)
context = new Button_1.ButtonContext(interaction, this);
break;
}
case DiscordTypes.ComponentType.ChannelSelect: {
if (structure)
context = new ChannelSelect_1.ChannelSelectContext(interaction, this);
break;
}
case DiscordTypes.ComponentType.MentionableSelect: {
if (structure)
context = new MentionableSelect_1.MentionableSelectContext(interaction, this);
break;
}
case DiscordTypes.ComponentType.RoleSelect: {
if (structure)
context = new RoleSelect_1.RoleSelectContext(interaction, this);
break;
}
case DiscordTypes.ComponentType.StringSelect: {
if (structure)
context = new StringSelect_1.StringSelectContext(interaction, this);
break;
}
case DiscordTypes.ComponentType.UserSelect: {
if (structure)
context = new UserSelect_1.UserSelectContext(interaction, this);
break;
}
}
break;
}
case DiscordTypes.InteractionType.ModalSubmit: {
structure = Array.from(this._boundModals).find((modal) => modal.getCustomId() === interaction.data.custom_id);
if (structure)
context = new Modal_1.ModalContext(interaction, this);
break;
}
}
if (!structure || !context)
return;
if (CommandHandler.isComponent(structure) ||
CommandHandler.isModal(structure)) {
const expire = Array.from(this._boundExpires).find((expire) => expire.structures.find((s) => s === structure));
if (expire)
expire.resetTimer();
}
this.client.log(`Running interaction ${interaction.id}`, {
level: `DEBUG`,
system: this.system,
});
try {
const middlewareCall = this._middleware(context, structure.getMiddlewareMeta());
let middlewareResult;
if (middlewareCall instanceof Promise) {
const reject = await middlewareCall.catch((error) => error);
if (reject instanceof Error)
throw reject;
else
middlewareResult = reject;
}
else {
middlewareResult = middlewareCall;
}
if (middlewareResult === false)
return;
const call = structure.getExecute()(context);
if (call instanceof Promise) {
const reject = await call.then(() => { }).catch((error) => error);
if (reject instanceof Error)
throw reject;
}
}
catch (error) {
try {
const call = this._error(context, error instanceof Error ? error : new Error(error));
if (call instanceof Promise) {
const reject = await call
.then(() => { })
.catch((error) => error);
if (reject instanceof Error)
throw reject;
}
}
catch (eError) {
this.client.log(`Unable to run error callback on interaction ${interaction.id}: ${eError?.message ?? eError ?? `Unknown reason`}`, {
level: `ERROR`,
system: this.system,
});
}
}
}
}
exports.CommandHandler = CommandHandler;
;