From 54014cb690eae8a6f89abf1695699c4b36ee398b Mon Sep 17 00:00:00 2001 From: Essem Date: Sat, 21 Mar 2026 21:39:54 -0500 Subject: [PATCH] refactor: Merge info and commands maps Lots of command handler refactoring here. A lot of logic was combined and simplified, and plenty of fixes have resulted from this. Speaking of which, this fixes a blocker that makes the next commit possible... --- commands/general/help.js | 2 +- commands/general/reload.js | 2 +- src/events/interactionCreate.ts | 22 ++-- src/events/messageCreate.ts | 28 ++--- src/utils/collections.ts | 9 +- src/utils/handler.ts | 188 +++++++++++++------------------- src/utils/help.ts | 6 +- src/utils/types.ts | 27 +---- 8 files changed, 111 insertions(+), 173 deletions(-) diff --git a/commands/general/help.js b/commands/general/help.js index 85bc2ebd..a3d406f6 100644 --- a/commands/general/help.js +++ b/commands/general/help.js @@ -23,7 +23,7 @@ class HelpCommand extends Command { ) { const joined = this.args.join(" ").toLowerCase(); const command = collections.aliases.get(joined) ?? joined; - const info = collections.info.get(command); + const info = collections.commands.get(command); if (!info) return this.getString("commands.responses.help.noInfo"); const params = info.params.filter((v) => typeof v === "string"); const embed = { diff --git a/commands/general/reload.js b/commands/general/reload.js index 47ec1b47..984963c7 100644 --- a/commands/general/reload.js +++ b/commands/general/reload.js @@ -17,7 +17,7 @@ class ReloadCommand extends Command { if (!skipSend) { await send(this.client); } - if (result !== commandName) return this.getString("commands.responses.reload.reloadFailed"); + if (result?.name !== commandName) return this.getString("commands.responses.reload.reloadFailed"); if (process.env.CLUSTER_TYPE) { process.send?.({ type: "process:msg", diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 657a7ba9..7ebbaf9d 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -35,19 +35,15 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac // check if command exists and if it's enabled const cmdBaseName = interaction.data.name; - const cmdBase = commands.get(cmdBaseName) ?? messageCommands.get(cmdBaseName) ?? userCommands.get(cmdBaseName); + let cmdName = cmdBaseName; + const sub = interaction.data.options.getSubCommand(); + if (sub) sub.map((v) => (cmdName += ` ${v}`)); + const cmdBase = commands.get(cmdName) ?? messageCommands.get(cmdName) ?? userCommands.get(cmdName); if (!cmdBase) return; - let command = cmdBaseName; - let cmd = cmdBase.default as typeof Command; + let cmd = cmdBase as typeof Command; if (!(cmd.prototype instanceof Command)) return; - const sub = interaction.data.options.getSubCommand(); - if (sub && cmdBase[sub[0]]?.prototype instanceof Command) { - cmd = cmdBase[sub[0]] as typeof Command; - command = `${command} ${sub[0]}`; - } - try { await interaction.defer(cmd.ephemeral || interaction.data.options.getBoolean("ephemeral", false) ? 64 : undefined); } catch (e) { @@ -63,7 +59,7 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac const invoker = interaction.member ?? interaction.user; // actually run the command - logger.log("main", `${invoker.username} (${invoker.id}) ran application command ${command}`); + logger.log("main", `${invoker.username} (${invoker.id}) ran application command ${cmdName}`); try { const commandClass = new cmd(client, database, { type: "application", interaction }); const result = await commandClass.run(); @@ -122,7 +118,7 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac ); } } else { - logger.debug(`Unknown return type for command ${command}: ${result} (${typeof result})`); + logger.debug(`Unknown return type for command ${cmdName}: ${result} (${typeof result})`); if (!result) return; await interaction.createFollowup( Object.assign( @@ -139,7 +135,7 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac Sentry.captureException(error, { tags: { process: process.env.pm_id ? Number.parseInt(process.env.pm_id) - 1 : 0, - command, + cmdName, args: JSON.stringify(interaction.data.options.raw), }, }); @@ -155,7 +151,7 @@ export default async ({ client, database }: EventParams, interaction: AnyInterac }); } else { logger.error( - `Error occurred with application command ${command} with arguments ${JSON.stringify(interaction.data.options.raw)}: ${(error as Error).stack || error}`, + `Error occurred with application command ${cmdName} with arguments ${JSON.stringify(interaction.data.options.raw)}: ${(error as Error).stack || error}`, ); try { await interaction.createFollowup({ diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 04282974..22883ae1 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -83,32 +83,32 @@ export default async ({ client, database }: EventParams, message: Message) => { const shifted = preArgs.shift(); if (!shifted) return; const cmdBaseName = shifted.toLowerCase(); - let aliased = aliases.get(cmdBaseName); - if (aliased?.includes(" ")) { - const subSplit = aliased.split(" "); - aliased = subSplit[0]; - preArgs.unshift(...subSplit.slice(1)); - } + const aliased = aliases.get(cmdBaseName); - const cmdName = aliased ?? cmdBaseName; + let cmdName = aliased ?? cmdBaseName; // check if command exists and if it's enabled const cmdBase = commands.get(cmdName); if (!cmdBase) return; let command = cmdBaseName; - let cmd = cmdBase.default as typeof Command; + let cmd = cmdBase as typeof Command; if (!(cmd.prototype instanceof Command)) return; // parse args const parsed = parseCommand(preArgs); let canon = cmdName; - const lowerSub = parsed.args[0]?.toLowerCase(); - if (cmdBase[lowerSub]?.prototype instanceof Command) { - cmd = cmdBase[lowerSub] as typeof Command; - canon = `${canon} ${lowerSub}`; - if (!aliased) command = `${command} ${lowerSub}`; - parsed.args = parsed.args.slice(1); + if (cmdBase.baseCommand) { + const lowerSub = parsed.args.map((v) => v.toLowerCase()); + for (const sub of lowerSub) { + const newCanon = `${canon} ${sub}`; + const subAlias = aliases.get(newCanon); + const subCmd = commands.get(subAlias ?? newCanon); + if (!subCmd) break; + cmd = subCmd as typeof Command; + canon = newCanon; + parsed.args = parsed.args.slice(1); + } } if (!cmd) return; diff --git a/src/utils/collections.ts b/src/utils/collections.ts index ef50259e..5085487a 100644 --- a/src/utils/collections.ts +++ b/src/utils/collections.ts @@ -1,14 +1,13 @@ import type InteractionCollector from "../pagination/awaitinteractions.ts"; import type { MediaMeta } from "./mediadetect.ts"; -import type { CommandEntry, CommandInfo } from "./types.ts"; +import type { ExtCommand } from "./types.ts"; -export const commands = new Map(); -export const messageCommands = new Map(); -export const userCommands = new Map(); +export const commands = new Map(); +export const messageCommands = new Map(); +export const userCommands = new Map(); export const paths = new Map(); export const aliases = new Map(); -export const info = new Map(); export const categories = new Map>(); export const collectors = new Map(); diff --git a/src/utils/handler.ts b/src/utils/handler.ts index 825a09b7..ffab998a 100644 --- a/src/utils/handler.ts +++ b/src/utils/handler.ts @@ -3,22 +3,22 @@ import { dirname, relative, resolve } from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { + type ApplicationCommandOptions, type Client, + type CombinedApplicationCommandOption, Constants, type CreateApplicationCommandOptions, type CreateGuildApplicationCommandOptions, } from "oceanic.js"; import Command from "#cmd-classes/command.js"; import commandConfig from "#config/commands.json" with { type: "json" }; -import { aliases, categories, commands, info, messageCommands, paths, userCommands } from "./collections.ts"; +import { aliases, categories, commands, messageCommands, paths, userCommands } from "./collections.ts"; import { getAllLocalizations } from "./i18n.ts"; -import { log } from "./logger.ts"; +import { debug, log } from "./logger.ts"; import type { - CommandEntry, CommandFlagType, - CommandInfo, CommandsConfig, - ConstructedCommandInfo, + ExtCommand, ExtendedCommandOptions, ExtendedConstructedCommandOptions, Param, @@ -34,34 +34,15 @@ const blacklist = (commandConfig as CommandsConfig).blacklist; /** * Load a command into memory. */ -export async function load(command: string, subcommand?: false): Promise; -export async function load( - command: string, - subcommand?: true, -): Promise< +export async function load(command: string): Promise< | { - props: typeof Command; - info: ConstructedCommandInfo; - entry: CommandEntry; - name: string; - } - | undefined ->; -export async function load( - command: string, - subcommand = false, -): Promise< - | string - | { - props: typeof Command; - info: ConstructedCommandInfo; - entry: CommandEntry; + props: ExtCommand; name: string; } | undefined > { log("main", `Loading command from ${command}...`); - const { default: props } = (await import(`${command}?v=${queryValue}`)) as { default: typeof Command }; + const { default: props } = (await import(`${command}?v=${queryValue}`)) as { default: ExtCommand }; queryValue++; const relPath = relative(cmdPath, command); @@ -92,77 +73,62 @@ export async function load( } props.init(); - - const extendedFlags = extendFlags(props.flags, fullCommandName); - - const commandInfo: CommandInfo = { - category: category, - description: props.description, - aliases: props.aliases, - params: parseFlags(props.flags), - flags: extendedFlags, - slashAllowed: props.slashAllowed, - directAllowed: props.directAllowed, - userAllowed: props.userAllowed, - baseCommand: false, - adminOnly: props.adminOnly, - type: Constants.ApplicationCommandTypes.CHAT_INPUT, - }; + props.baseCommand = false; + props.category = category; + props.type = Constants.ApplicationCommandTypes.CHAT_INPUT; + props.params = parseFlags(props.flags); + props.flags = extendFlags(props.flags, fullCommandName); paths.set(fullCommandName, command); - const cmdMap: CommandEntry = { - default: props, - }; + if (category === "message") { + messageCommands.set(fullCommandName, props); + props.type = Constants.ApplicationCommandTypes.MESSAGE; + } else if (category === "user") { + userCommands.set(fullCommandName, props); + props.type = Constants.ApplicationCommandTypes.USER; + } else { + const subdir = relPath.split(".")[0]; + const resolved = resolve(cmdPath, subdir); - if (!subcommand) { - if (commandInfo.category === "message") { - messageCommands.set(commandName, cmdMap); - commandInfo.type = Constants.ApplicationCommandTypes.MESSAGE; - } else if (commandInfo.category === "user") { - userCommands.set(commandName, cmdMap); - commandInfo.type = Constants.ApplicationCommandTypes.USER; - } else { - try { - const subdir = relPath.split(".")[0]; - const resolved = resolve(cmdPath, subdir); - const files = await readdir(resolved, { - withFileTypes: true, - }); - commandInfo.baseCommand = true; - commandInfo.flags = []; - for (const file of files) { - if (!file.isFile()) continue; - const sub = await load(resolve(resolved, file.name), true); - if (!sub) continue; + let files; + try { + files = await readdir(resolved, { + withFileTypes: true, + }); + } catch { + debug(`Could not find subcommand dir at ${resolved}`); + } + + if (files) { + props.baseCommand = true; + props.flags = []; + for (const file of files) { + if (!file.isFile()) continue; + const sub = await load(resolve(resolved, file.name)); + if (!sub) continue; - const split = sub.name.split(" "); - const subName = split[split.length - 1]; - cmdMap[subName] = sub.props; + const split = sub.name.split(" "); + const subName = split[split.length - 1]; - const hasSubCommands = sub.info.flags.some( - (v) => v.type === Constants.ApplicationCommandOptionTypes.SUB_COMMAND || v.type === "subcommand", - ); - commandInfo.flags.push({ - name: subName, - nameLocalizations: getAllLocalizations(`commands.flagNames.${fullCommandName}.${subName}`), - type: hasSubCommands - ? Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP - : Constants.ApplicationCommandOptionTypes.SUB_COMMAND, - description: sub.info.description, - descriptionLocalizations: getAllLocalizations(`commands.flags.${fullCommandName}.${subName}`), - // @ts-expect-error It thinks we're using the wrong flag type - options: sub.info.flags, - }); - } - } catch { - // come back to this + const hasSubCommands = sub.props.flags.some( + (v) => v.type === Constants.ApplicationCommandOptionTypes.SUB_COMMAND || v.type === "subcommand", + ); + props.flags.push({ + name: subName, + nameLocalizations: getAllLocalizations(`commands.flagNames.${fullCommandName}.${subName}`), + type: hasSubCommands + ? Constants.ApplicationCommandOptionTypes.SUB_COMMAND_GROUP + : Constants.ApplicationCommandOptionTypes.SUB_COMMAND, + description: sub.props.description, + descriptionLocalizations: getAllLocalizations(`commands.flags.${fullCommandName}.${subName}`), + options: sub.props.flags as CombinedApplicationCommandOption[], + }); } - commands.set(commandName, cmdMap); } - } - info.set(fullCommandName, commandInfo); + commands.set(fullCommandName, props); + } const categoryCommands = categories.get(category) ?? new Set(); categoryCommands.add(fullCommandName); @@ -175,14 +141,10 @@ export async function load( } } - return subcommand - ? { - props, - info: commandInfo, - entry: cmdMap, - name: fullCommandName, - } - : fullCommandName; + return { + props, + name: fullCommandName, + }; } export const flagMap: Array = [ @@ -256,29 +218,27 @@ export function update() { const commandArray: CreateApplicationCommandOptions[] = []; const privateCommandArray: CreateApplicationCommandOptions[] = []; const merged = new Map([...commands, ...messageCommands, ...userCommands]); - for (const name of merged.keys()) { - const cmdInfo = info.get(name); - if ( - cmdInfo?.type === Constants.ApplicationCommandTypes.MESSAGE || - cmdInfo?.type === Constants.ApplicationCommandTypes.USER - ) { - (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ + for (const [name, cmd] of merged) { + // skip slash commands with spaces in the title + if (cmd.type === Constants.ApplicationCommandTypes.CHAT_INPUT && name.includes(" ")) continue; + if (cmd.type === Constants.ApplicationCommandTypes.MESSAGE || cmd.type === Constants.ApplicationCommandTypes.USER) { + (cmd.adminOnly ? privateCommandArray : commandArray).push({ name: name, nameLocalizations: getAllLocalizations(`commands.names.${name}`), - type: cmdInfo.type, - integrationTypes: [0, cmdInfo.userAllowed ? 1 : null].filter((v) => v !== null), - contexts: [0, cmdInfo.directAllowed ? 1 : null, 2].filter((v) => v !== null), + type: cmd.type, + integrationTypes: [0, cmd.userAllowed ? 1 : null].filter((v) => v !== null), + contexts: [0, cmd.directAllowed ? 1 : null, 2].filter((v) => v !== null), }); - } else if (cmdInfo?.slashAllowed) { - (cmdInfo.adminOnly ? privateCommandArray : commandArray).push({ + } else if (cmd.slashAllowed) { + (cmd.adminOnly ? privateCommandArray : commandArray).push({ name, nameLocalizations: getAllLocalizations(`commands.names.${name}`), - type: cmdInfo.type.valueOf(), - description: cmdInfo.description, + type: cmd.type.valueOf(), + description: cmd.description, descriptionLocalizations: getAllLocalizations(`commands.descriptions.${name}`), - options: cmdInfo.flags, - integrationTypes: [0, cmdInfo.userAllowed ? 1 : null].filter((v) => v !== null), - contexts: [0, cmdInfo.directAllowed ? 1 : null, 2].filter((v) => v !== null), + options: cmd.flags as ApplicationCommandOptions[], + integrationTypes: [0, cmd.userAllowed ? 1 : null].filter((v) => v !== null), + contexts: [0, cmd.directAllowed ? 1 : null, 2].filter((v) => v !== null), }); } } diff --git a/src/utils/help.ts b/src/utils/help.ts index 859c2d7f..2e0248b1 100644 --- a/src/utils/help.ts +++ b/src/utils/help.ts @@ -1,7 +1,7 @@ import { promises } from "node:fs"; import process from "node:process"; import commandConfig from "#config/commands.json" with { type: "json" }; -import { info } from "./collections.ts"; +import { commands } from "./collections.ts"; import type { Param } from "./types.ts"; export const categoryTemplate = { @@ -34,10 +34,8 @@ function generateEntries(baseName: string, params: Param[], desc: string, catego export function generateList() { categories = categoryTemplate; - for (const [command, cmd] of info) { + for (const [command, cmd] of commands) { if (!cmd) throw Error(`Command info missing for ${command}`); - // reject non-chat commands - if (cmd.type !== 1) continue; if (!cmd.slashAllowed && !commandConfig.types.classic) continue; if (cmd.baseCommand) continue; if (!categories[cmd.category]) categories[cmd.category] = []; diff --git a/src/utils/types.ts b/src/utils/types.ts index 69794c17..ca9ca5a4 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -29,9 +29,12 @@ export interface CommandsConfig { blacklist: string[]; } -type ValueOrNested = T | { [x: string]: ValueOrNested }; - -export type CommandEntry = Record>; +export type ExtCommand = { + baseCommand: boolean; + category: string; + params: Param[]; + type: Constants.ApplicationCommandTypes; +} & typeof Command; export type CommandType = "classic" | "application"; export type CommandFlagType = @@ -55,10 +58,6 @@ export type ExtendedConstructedCommandOptions = { classic?: boolean; } & Omit; -export type ConstructedCommandInfo = { - flags: ExtendedConstructedCommandOptions[]; -} & Omit; - export type Param = | { name: string; @@ -67,20 +66,6 @@ export type Param = } | string; -export interface CommandInfo { - category: string; - description: string; - aliases: string[]; - params: Param[]; - flags: ExtendedCommandOptions[]; - slashAllowed: boolean; - directAllowed: boolean; - userAllowed: boolean; - baseCommand: boolean; - adminOnly: boolean; - type: Constants.ApplicationCommandTypes; -} - export interface MediaParams { cmd: string; type: "image"; -- 2.51.2