Adds eslint and linted & improved routing for interactions

This commit is contained in:
Michel Fedde 2025-06-17 20:37:53 +02:00
parent 83209f642c
commit 441715675c
35 changed files with 2091 additions and 463 deletions

View file

@ -10,7 +10,7 @@ export class CommandDeployer {
}
public async deployAvailableServers() {
const commandInfos = [];
const commandInfos: object[] = [];
this.client.Commands.allCommands.forEach((command) => {
commandInfos.push(command.definition().toJSON())
})
@ -27,11 +27,10 @@ export class CommandDeployer {
this.logger.log(`Started refreshing ${commandInfos.length} application (/) commands for ${serverId}.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await this.client.RESTClient.put(
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
{ body: commandInfos },
await this.client.RESTClient.put(
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
{ body: commandInfos },
);
this.logger.log(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
}
}

View file

@ -1,9 +1,7 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
CommandInteraction,
GuildMember, SlashCommandIntegerOption,
SlashCommandStringOption
} from "discord.js";
import {Container} from "../../Container/Container";
import {GroupRepository} from "../../Repositories/GroupRepository";
@ -22,7 +20,7 @@ export class GroupSelection {
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
const value = interaction.options.getFocused();
const repo = Container.get<GroupRepository>(GroupRepository.name);
let groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
await interaction.respond(
groups
.filter((group) => group.name.startsWith(value))

View file

@ -1,5 +1,4 @@
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
import Commands from "./Commands";
export interface Command {
definition(): SlashCommandBuilder;

View file

@ -1,12 +1,12 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
SlashCommandBuilder,
ChatInputCommandInteraction,
MessageFlags,
InteractionReplyOptions,
GuildMember,
EmbedBuilder,
AutocompleteInteraction,
roleMention, time, userMention
roleMention, time, userMention, GuildMemberRoleManager
} from "discord.js";
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
import {GroupModel} from "../../Models/GroupModel";
@ -19,265 +19,264 @@ import {GroupConfigurationRenderer} from "../../Groups/GroupConfigurationRendere
import {GroupConfigurationHandler} from "../../Groups/GroupConfigurationHandler";
import {GroupConfigurationTransformers} from "../../Groups/GroupConfigurationTransformers";
import {GroupConfigurationRepository} from "../../Repositories/GroupConfigurationRepository";
import {IconCache} from "../../Icons/IconCache";
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
import {Nullable} from "../../types/Nullable";
export class GroupCommand implements Command, ChatInteractionCommand, AutocompleteCommand {
private static GOODBYE_MESSAGES: string[] = [
'Sad to see you go.',
'May your next adventure be fruitful.',
'I hope, I served you well.',
'I wish you and your group good luck on your next adventures.',
]
private static INVALID_CHARACTER_SEQUENCES: string[] = [
"http://",
"https://"
]
definition(): SlashCommandBuilder {
// @ts-ignore
return new SlashCommandBuilder()
.setName('groups')
.setDescription(`Manages groups`)
.addSubcommand(create =>
create.setName("create")
.setDescription("Creates a new group, with executing user being the leader")
.addStringOption((option) =>
option.setName("name")
.setDescription("Defines the name for the group.")
.setRequired(true)
.setMaxLength(64)
)
.addRoleOption((builder) =>
builder.setName("role")
.setDescription("Defines the role, where all the members are located in.")
.setRequired(true)
)
)
.addSubcommand(listCommand =>
listCommand
.setName("list")
.setDescription("Displays the groups you are apart of.")
)
.addSubcommand(command => command
.setName('config')
.setDescription("Starts the config manager for the group.")
.addIntegerOption(GroupSelection.createOptionSetup())
)
.addSubcommand(command => command
.setName("remove")
.setDescription("Deletes a group you are the leader for.")
.addIntegerOption(GroupSelection.createOptionSetup())
)
.addSubcommand(command => command
.setName("transfer")
.setDescription("Transfers leadership of a group to a different person")
.addIntegerOption(GroupSelection.createOptionSetup)
.addUserOption(option => option
.setName("target")
.setDescription("The member, that is the new leader")
.setRequired(true)
)
);
}
private static GOODBYE_MESSAGES: string[] = [
'Sad to see you go.',
'May your next adventure be fruitful.',
'I hope, I served you well.',
'I wish you and your group good luck on your next adventures.',
]
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
switch (interaction.options.getSubcommand()) {
case "create":
this.create(interaction);
break;
case "list":
this.list(interaction);
break;
case "remove":
await this.remove(interaction);
break;
case "config":
await this.runConfigurator(interaction);
break;
case "transfer":
await this.transferLeadership(interaction);
break;
default:
throw new Error("Unsupported command");
}
return Promise.resolve();
}
private create(interaction: ChatInputCommandInteraction): void {
const name = interaction.options.getString("name") ?? '';
const role = interaction.options.getRole("role", true);
if (role.id === interaction.guildId) {
throw new UserError("Creating a group for everyone is not permitted!");
}
if (!interaction.member?.roles.cache.has(role?.id) ?? true) {
throw new UserError(
"You are not part of the role, you try to create a group for.",
"Add yourself to the group or ask your admin to do so."
);
}
const validName = this.validateGroupName(name);
if (name !== true) {
throw new UserError(`Your group name contains one or more invalid character sequences: ${validName}`)
}
const group: GroupModel = {
id: -1,
name: name,
leader: {
server: interaction.guildId ?? '',
memberid: interaction.member?.user.id ?? ''
},
role: {
server: interaction.guildId ?? '',
roleid: role?.id ?? ''
}
}
Container.get<GroupRepository>(GroupRepository.name).create(group);
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
}
private validateGroupName(name: string): true|string{
const lowercaseName = name.toLowerCase();
for (let invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
if (!lowercaseName.includes(invalidcharactersequence)) {
continue
}
return invalidcharactersequence
}
return true;
}
private static INVALID_CHARACTER_SEQUENCES: string[] = [
"http://",
"https://"
]
private list(interaction: ChatInputCommandInteraction) {
const repo = Container.get<GroupRepository>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
const iconCache = Container.get<IconCache>(IconCache.name);
const embed = new EmbedBuilder()
.setTitle("Your groups on this server:")
.setFields(
groups.map(group => {
const nextPlaydate = playdateRepo.getNextPlaydateForGroup(group);
const values = [
`Role: ${iconCache.getEmoji("people_group_solid")} ${roleMention(group.role.roleid)}`,
`Leader/GM: ${userMention(group.leader.memberid)}`
];
if (nextPlaydate) {
values.push(
`Next Playdate: ${iconCache.getEmoji("calendar_days_solid")} ${time(nextPlaydate.from_time, "F")}`
)
}
return {
name: group.name,
value: values.join("\n")
}
})
)
const reply: InteractionReplyOptions = {
embeds: [
embed
],
allowedMentions: { roles: [] },
flags: MessageFlags.Ephemeral
}
interaction.reply(reply);
}
private async remove(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const repo = Container.get<GroupRepository>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError("Can't remove group. You are not the leader.");
}
repo.deleteGroup(group);
const embed = new EmbedBuilder()
.setTitle("Group deleted.")
.setDescription(
`:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`
)
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
})
}
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
const option = interaction.options.getFocused(true);
if (option.name == "group") {
await GroupSelection.handleAutocomplete(interaction, true);
return;
}
}
private async runConfigurator(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const configurationRenderer = new GroupConfigurationRenderer(
new GroupConfigurationHandler(
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
group
),
new GroupConfigurationTransformers(),
definition(): SlashCommandBuilder {
// @ts-expect-error Slash command expects more than needed.
return new SlashCommandBuilder()
.setName('groups')
.setDescription(`Manages groups`)
.addSubcommand(create =>
create.setName("create")
.setDescription("Creates a new group, with executing user being the leader")
.addStringOption((option) =>
option.setName("name")
.setDescription("Defines the name for the group.")
.setRequired(true)
.setMaxLength(64)
)
.addRoleOption((builder) =>
builder.setName("role")
.setDescription("Defines the role, where all the members are located in.")
.setRequired(true)
)
)
.addSubcommand(listCommand =>
listCommand
.setName("list")
.setDescription("Displays the groups you are apart of.")
)
.addSubcommand(command => command
.setName('config')
.setDescription("Starts the config manager for the group.")
.addIntegerOption(GroupSelection.createOptionSetup())
)
.addSubcommand(command => command
.setName("remove")
.setDescription("Deletes a group you are the leader for.")
.addIntegerOption(GroupSelection.createOptionSetup())
)
.addSubcommand(command => command
.setName("transfer")
.setDescription("Transfers leadership of a group to a different person")
.addIntegerOption(GroupSelection.createOptionSetup)
.addUserOption(option => option
.setName("target")
.setDescription("The member, that is the new leader")
.setRequired(true)
)
await configurationRenderer.setup(interaction);
);
}
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
switch (interaction.options.getSubcommand()) {
case "create":
this.create(interaction);
break;
case "list":
this.list(interaction);
break;
case "remove":
await this.remove(interaction);
break;
case "config":
await this.runConfigurator(interaction);
break;
case "transfer":
await this.transferLeadership(interaction);
break;
default:
throw new Error("Unsupported command");
}
private async transferLeadership(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
return Promise.resolve();
}
const repo = Container.get<GroupRepository>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError(
"Can't transfer leadership. You are not the leader."
);
}
const newLeader = <GuildMember>interaction.options.getMember("target", true);
if (!newLeader.roles.cache.has(group.role.roleid)) {
throw new UserError(
"Can't transfer leadership: The target member is not part of your group.",
"Add the user to the role this group is part in or ask your server admin to do it."
);
}
group.leader.memberid = newLeader.id
repo.update(group);
private create(interaction: ChatInputCommandInteraction): void {
const name = interaction.options.getString("name") ?? '';
const role = interaction.options.getRole("role", true);
if (role.id === interaction.guildId) {
throw new UserError("Creating a group for everyone is not permitted!");
}
const embed = new EmbedBuilder()
.setTitle("Leadership transfered")
.setDescription(
`Leadership was successfully transfered to ${userMention(newLeader.user.id)}`
if (!(<Nullable<GuildMemberRoleManager>>interaction.member?.roles)?.cache.has(role?.id)) {
throw new UserError(
"You are not part of the role, you try to create a group for.",
"Add yourself to the group or ask your admin to do so."
);
}
const validName = this.validateGroupName(name);
// @ts-expect-error Is correct, since the valid name can return either true or a string and the error should only be thrown if it's a string.
if (name !== true) {
throw new UserError(`Your group name contains one or more invalid character sequences: ${validName}`)
}
const group: GroupModel = {
id: -1,
name: name,
leader: {
server: interaction.guildId ?? '',
memberid: interaction.member?.user.id ?? ''
},
role: {
server: interaction.guildId ?? '',
roleid: role?.id ?? ''
}
}
Container.get<GroupRepository>(GroupRepository.name).create(group);
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
}
private validateGroupName(name: string): true|string{
const lowercaseName = name.toLowerCase();
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
if (!lowercaseName.includes(invalidcharactersequence)) {
continue
}
return invalidcharactersequence
}
return true;
}
private list(interaction: ChatInputCommandInteraction) {
const repo = Container.get<GroupRepository>(GroupRepository.name);
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
const embed = new EmbedBuilder()
.setTitle("Your groups on this server:")
.setFields(
groups.map(group => {
const nextPlaydate = playdateRepo.getNextPlaydateForGroup(group);
const values = [
`Role: ${roleMention(group.role.roleid)}`,
`Leader/GM: ${userMention(group.leader.memberid)}`
];
if (nextPlaydate) {
values.push(
`Next Playdate: ${time(nextPlaydate.from_time, "F")}`
)
}
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
return {
name: group.name,
value: values.join("\n")
}
})
)
const reply: InteractionReplyOptions = {
embeds: [
embed
],
allowedMentions: { roles: [] },
flags: MessageFlags.Ephemeral
}
interaction.reply(reply);
}
private async remove(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const repo = Container.get<GroupRepository>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError("Can't remove group. You are not the leader.");
}
repo.deleteGroup(group);
const embed = new EmbedBuilder()
.setTitle("Group deleted.")
.setDescription(
`:x: Deleted \`${group.name}\`. ${ArrayUtils.chooseRandom(GroupCommand.GOODBYE_MESSAGES)}`
)
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
})
}
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
const option = interaction.options.getFocused(true);
if (option.name == "group") {
await GroupSelection.handleAutocomplete(interaction, true);
return;
}
}
private async runConfigurator(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const configurationRenderer = new GroupConfigurationRenderer(
new GroupConfigurationHandler(
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
group
),
new GroupConfigurationTransformers(),
)
await configurationRenderer.setup(interaction);
}
private async transferLeadership(interaction: ChatInputCommandInteraction) {
const group = GroupSelection.getGroup(interaction);
const repo = Container.get<GroupRepository>(GroupRepository.name);
if (group.leader.memberid != interaction.member?.user.id) {
throw new UserError(
"Can't transfer leadership. You are not the leader."
);
}
const newLeader = <GuildMember>interaction.options.getMember("target");
if (!newLeader.roles.cache.has(group.role.roleid)) {
throw new UserError(
"Can't transfer leadership: The target member is not part of your group.",
"Add the user to the role this group is part in or ask your server admin to do it."
);
}
group.leader.memberid = newLeader.id
repo.update(group);
const embed = new EmbedBuilder()
.setTitle("Leadership transferred")
.setDescription(
`Leadership was successfully transferred to ${userMention(newLeader.user.id)}`
)
await interaction.reply({
embeds: [
embed
],
flags: MessageFlags.Ephemeral,
})
}
}

View file

@ -1,4 +1,4 @@
import {SlashCommandBuilder, Interaction, CommandInteraction} from "discord.js";
import {SlashCommandBuilder, CommandInteraction} from "discord.js";
import {Command} from "./Command";
export class HelloWorldCommand implements Command {
@ -6,7 +6,7 @@ export class HelloWorldCommand implements Command {
'Hello :)',
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
'Roll for initiative!',
'I was an adventurerer like you...',
'I was an adventurer like you...',
'Hello :) How are you?',
]

View file

@ -1,22 +1,16 @@
import {
SlashCommandBuilder,
Interaction,
CommandInteraction,
AutocompleteInteraction,
GuildMember,
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, ModalSubmitFields, time, User
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time
} from "discord.js";
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
import {Container} from "../../Container/Container";
import {GroupRepository} from "../../Repositories/GroupRepository";
import {GroupSelection} from "../CommandPartials/GroupSelection";
import {setFlagsFromString} from "node:v8";
import {UserError} from "../UserError";
import Playdate from "../../Database/tables/Playdate";
import {PlaydateModel} from "../../Models/PlaydateModel";
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
import {GroupModel} from "../../Models/GroupModel";
import playdate from "../../Database/tables/Playdate";
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
static REGEX = [
@ -24,7 +18,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
]
definition(): SlashCommandBuilder {
// @ts-ignore
// @ts-expect-error Command builder is improperly marked as incomplete.
return new SlashCommandBuilder()
.setName("playdates")
.setDescription("Manage your playdates")
@ -106,7 +100,7 @@ export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInter
to_time: new Date(toDate),
}
const id = playdateRepo.create(playdate);
playdateRepo.create(playdate);
const embed = new EmbedBuilder()
.setTitle("Created a play-date.")

View file

@ -2,142 +2,66 @@ import {
Client,
GatewayIntentBits,
Events,
Interaction,
ChatInputCommandInteraction,
MessageFlags,
Activity,
ActivityType, REST, inlineCode
ActivityType, REST
} from "discord.js";
import Commands from "./Commands/Commands";
import {Container} from "../Container/Container";
import {Logger} from "log4js";
import {UserError} from "./UserError";
import {InteractionRouter} from "./InteractionRouter";
export class DiscordClient {
private readonly client: Client;
private commands: Commands;
private readonly restClient: REST;
public get Client (): Client {
public get Client(): Client {
return this.client;
}
public get Commands(): Commands {
return this.commands
public get Commands(): Commands {
return this.router.commands
}
public get RESTClient(): REST {
return this.restClient;
}
public get ApplicationId(): string {
return this.applicationId;
}
constructor(
private readonly applicationId: string
private readonly applicationId: string,
private readonly router: InteractionRouter,
private readonly restClient: REST = new REST()
) {
this.client = new Client({
intents: [GatewayIntentBits.Guilds]
})
this.commands = new Commands();
this.restClient = new REST();
}
applyEvents() {
this.client.once(Events.ClientReady, () => {
if (!this.client.user) {
return;
}
Container.get<Logger>("logger").info(`Ready! Logged in as ${this.client.user.tag}`);
this.client.user.setActivity('your PnP playdates', {
type: ActivityType.Watching,
});
})
this.client.on(Events.GuildAvailable, () => {
Container.get<Logger>("logger").info("Joined Guild?")
})
this.client.on(Events.InteractionCreate, async (interaction: Interaction) => {
const method = this.findCommandMethod(interaction);
if (!method) {
Container.get<Logger>("logger").error(`Could not find method for '${interaction.commandName}'`);
return;
}
await method();
})
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
}
connect(token: string) {
this.client.login(token);
}
connectRESTClient(token: string) {
this.restClient.setToken(token);
}
private findCommandMethod(interaction: Interaction) {
if (interaction.isChatInputCommand()) {
const command = this.commands.getCommand(interaction.commandName);
if (!command) {
return null;
}
if (!('execute' in command)) {
return null;
}
return async () => {
Container.get<Logger>("logger").debug(`Found chat command ${interaction.commandName}: running...`);
try {
await command.execute(interaction)
}
catch (e: any) {
Container.get<Logger>("logger").error(e)
let userMessage = ":x: There was an error while executing this command!";
if (e.constructor.name === UserError.name) {
userMessage = `:x: \`${e.message}\` - Please validate your request!`
if (e.tryInstead) {
userMessage += `
You can try the following:
${inlineCode(e.tryInstead)}`
}
}
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
}
}
}
}
if (interaction.isAutocomplete()) {
const command = this.commands.getCommand(interaction.commandName);
if (!command) {
return null;
}
if (!('handleAutocomplete' in command)) {
return null;
}
return async () => {
Container.get<Logger>("logger").debug(`Found command ${interaction.commandName} for autocomplete: handling...`);
try {
await command.handleAutocomplete(interaction);
} catch (e: any) {
Container.get<Logger>('logger').error(e);
}
}
}
return null;
}
}

View file

@ -0,0 +1,109 @@
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
inlineCode,
Interaction,
MessageFlags,
} from "discord.js";
import Commands from "./Commands/Commands";
import {Logger} from "log4js";
import {UserError} from "./UserError";
import {Container} from "../Container/Container";
enum InteractionRoutingType {
Unrouted,
Command,
AutoComplete,
}
export class InteractionRouter {
constructor(
public readonly commands: Commands,
public readonly logger: Logger
) {
}
async route(interaction: Interaction) {
const interactionType = this.findInteractionType(interaction);
switch (interactionType) {
case InteractionRoutingType.Unrouted:
this.logger.debug("Unroutable interaction found...")
break;
case InteractionRoutingType.Command:
await this.handleCommand(<ChatInputCommandInteraction>interaction);
break;
case InteractionRoutingType.AutoComplete:
await this.handleAutocomplete(<AutocompleteInteraction>interaction)
break;
}
}
private findInteractionType(interaction: Interaction): InteractionRoutingType {
if (interaction.isChatInputCommand()) {
return InteractionRoutingType.Command;
}
if (interaction.isAutocomplete()) {
return InteractionRoutingType.AutoComplete;
}
return InteractionRoutingType.Unrouted;
}
private async handleCommand(interaction: ChatInputCommandInteraction) {
try {
const command = this.commands.getCommand(interaction.commandName);
if (!command) {
throw new UserError(`Requested command not found.`);
}
if (!('execute' in command)) {
throw new UserError(`Requested command is not setup for a chat command.`);
}
this.logger.debug(`Found chat command ${interaction.commandName}: running...`);
await command.execute?.call(command, interaction);
} catch (e: any) {
this.logger.error(e)
let userMessage = ":x: There was an error while executing this command!";
if (e.constructor.name === UserError.name) {
userMessage = `:x: \`${e.message}\` - Please validate your request!`
if (e.tryInstead) {
userMessage += `
You can try the following:
${inlineCode(e.tryInstead)}`
}
}
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
}
}
}
private async handleAutocomplete(interaction: AutocompleteInteraction) {
const command = this.commands.getCommand(interaction.commandName);
if (!command) {
return null;
}
if (!('handleAutocomplete' in command)) {
return null;
}
Container.get<Logger>("logger").debug(`Found command ${interaction.commandName} for autocomplete: handling...`);
try {
await command.handleAutocomplete?.call(command, interaction);
} catch (e: unknown) {
Container.get<Logger>('logger').error(e);
}
}
}