182 lines
7.2 KiB
TypeScript
182 lines
7.2 KiB
TypeScript
import {
|
|
AutocompleteInteraction,
|
|
ChatInputCommandInteraction,
|
|
CommandInteraction,
|
|
NewsChannel,
|
|
TextChannel,
|
|
ThreadChannel,
|
|
} from 'discord.js';
|
|
import { RateLimiter } from 'discord.js-rate-limiter';
|
|
import { createRequire } from 'node:module';
|
|
|
|
import { EventHandler } from './index.js';
|
|
import { Command, CommandDeferType } from '../commands/index.js';
|
|
import { DiscordLimits } from '../constants/index.js';
|
|
import { EventData } from '../models/internal-models.js';
|
|
import { EventDataService, Lang, Logger } from '../services/index.js';
|
|
import { CommandUtils, InteractionUtils } from '../utils/index.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
let Config = require('../../config/config.json');
|
|
let Logs = require('../../lang/logs.json');
|
|
|
|
export class CommandHandler implements EventHandler {
|
|
private rateLimiter = new RateLimiter(
|
|
Config.rateLimiting.commands.amount,
|
|
Config.rateLimiting.commands.interval * 1000
|
|
);
|
|
|
|
constructor(public commands: Command[], private eventDataService: EventDataService) {}
|
|
|
|
public async process(intr: CommandInteraction | AutocompleteInteraction): Promise<void> {
|
|
// Don't respond to self, or other bots
|
|
if (intr.user.id === intr.client.user?.id || intr.user.bot) {
|
|
return;
|
|
}
|
|
|
|
let commandParts =
|
|
intr instanceof ChatInputCommandInteraction || intr instanceof AutocompleteInteraction
|
|
? [
|
|
intr.commandName,
|
|
intr.options.getSubcommandGroup(false),
|
|
intr.options.getSubcommand(false),
|
|
].filter(Boolean)
|
|
: [intr.commandName];
|
|
let commandName = commandParts.join(' ');
|
|
|
|
// Try to find the command the user wants
|
|
let command = CommandUtils.findCommand(this.commands, commandParts);
|
|
if (!command) {
|
|
Logger.error(
|
|
Logs.error.commandNotFound
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (intr instanceof AutocompleteInteraction) {
|
|
if (!command.autocomplete) {
|
|
Logger.error(
|
|
Logs.error.autocompleteNotFound
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let option = intr.options.getFocused(true);
|
|
let choices = await command.autocomplete(intr, option);
|
|
await InteractionUtils.respond(
|
|
intr,
|
|
choices?.slice(0, DiscordLimits.CHOICES_PER_AUTOCOMPLETE)
|
|
);
|
|
} catch (error) {
|
|
Logger.error(
|
|
intr.channel instanceof TextChannel ||
|
|
intr.channel instanceof NewsChannel ||
|
|
intr.channel instanceof ThreadChannel
|
|
? Logs.error.autocompleteGuild
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{OPTION_NAME}', commandName)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
.replaceAll('{USER_TAG}', intr.user.tag)
|
|
.replaceAll('{USER_ID}', intr.user.id)
|
|
.replaceAll('{CHANNEL_NAME}', intr.channel.name)
|
|
.replaceAll('{CHANNEL_ID}', intr.channel.id)
|
|
.replaceAll('{GUILD_NAME}', intr.guild?.name)
|
|
.replaceAll('{GUILD_ID}', intr.guild?.id)
|
|
: Logs.error.autocompleteOther
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{OPTION_NAME}', commandName)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
.replaceAll('{USER_TAG}', intr.user.tag)
|
|
.replaceAll('{USER_ID}', intr.user.id),
|
|
error
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check if user is rate limited
|
|
let limited = this.rateLimiter.take(intr.user.id);
|
|
if (limited) {
|
|
return;
|
|
}
|
|
|
|
// Defer interaction
|
|
// NOTE: Anything after this point we should be responding to the interaction
|
|
switch (command.deferType) {
|
|
case CommandDeferType.PUBLIC: {
|
|
await InteractionUtils.deferReply(intr, false);
|
|
break;
|
|
}
|
|
case CommandDeferType.HIDDEN: {
|
|
await InteractionUtils.deferReply(intr, true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Return if defer was unsuccessful
|
|
if (command.deferType !== CommandDeferType.NONE && !intr.deferred) {
|
|
return;
|
|
}
|
|
|
|
// Get data from database
|
|
let data = await this.eventDataService.create({
|
|
user: intr.user,
|
|
channel: intr.channel,
|
|
guild: intr.guild,
|
|
args: intr instanceof ChatInputCommandInteraction ? intr.options : undefined,
|
|
});
|
|
|
|
try {
|
|
// Check if interaction passes command checks
|
|
let passesChecks = await CommandUtils.runChecks(command, intr, data);
|
|
if (passesChecks) {
|
|
// Execute the command
|
|
await command.execute(intr, data);
|
|
}
|
|
} catch (error) {
|
|
await this.sendError(intr, data);
|
|
|
|
// Log command error
|
|
Logger.error(
|
|
intr.channel instanceof TextChannel ||
|
|
intr.channel instanceof NewsChannel ||
|
|
intr.channel instanceof ThreadChannel
|
|
? Logs.error.commandGuild
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
.replaceAll('{USER_TAG}', intr.user.tag)
|
|
.replaceAll('{USER_ID}', intr.user.id)
|
|
.replaceAll('{CHANNEL_NAME}', intr.channel.name)
|
|
.replaceAll('{CHANNEL_ID}', intr.channel.id)
|
|
.replaceAll('{GUILD_NAME}', intr.guild?.name)
|
|
.replaceAll('{GUILD_ID}', intr.guild?.id)
|
|
: Logs.error.commandOther
|
|
.replaceAll('{INTERACTION_ID}', intr.id)
|
|
.replaceAll('{COMMAND_NAME}', commandName)
|
|
.replaceAll('{USER_TAG}', intr.user.tag)
|
|
.replaceAll('{USER_ID}', intr.user.id),
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async sendError(intr: CommandInteraction, data: EventData): Promise<void> {
|
|
try {
|
|
await InteractionUtils.send(
|
|
intr,
|
|
Lang.getEmbed('errorEmbeds.command', data.lang, {
|
|
ERROR_CODE: intr.id,
|
|
GUILD_ID: intr.guild?.id ?? Lang.getRef('other.na', data.lang),
|
|
SHARD_ID: (intr.guild?.shardId ?? 0).toString(),
|
|
})
|
|
);
|
|
} catch {
|
|
// Ignore
|
|
}
|
|
}
|
|
}
|