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 { // 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 { 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 } } }