From 1f08fd28f3975e03939842fa959f5e5b282df0f5 Mon Sep 17 00:00:00 2001 From: zleyyij Date: Sat, 26 Aug 2023 20:04:02 -0600 Subject: [PATCH] core, factoids: added autocomplete to options --- src/core/main.ts | 35 ++++++++++++++++++++++++++++++++ src/core/modules.ts | 22 ++++++++++++++++---- src/core/slash_commands.ts | 12 +++++++---- src/modules/factoids/factoids.ts | 26 ++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/core/main.ts b/src/core/main.ts index 51525db..96bcc58 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -57,6 +57,41 @@ client.once(Events.ClientReady, async () => { ); }); +// when the bot receives an autocomplete interaction, figure out which module it's autocompleting for, and +// call that module's autocomplete code +client.on(Events.InteractionCreate, async interaction => { + if (!interaction.isAutocomplete()) { + return; + } + const input = interaction.options.getFocused(true); + const command = interaction.commandName; + const group = interaction.options.getSubcommandGroup(); + const subcommand = interaction.options.getSubcommand(false); + + const commandPath: string[] = []; + // the command is always defined + commandPath.push(command); + if (group !== null) { + commandPath.push(group); + } + if (subcommand !== null) { + commandPath.push(subcommand); + } + // non-null-assertion: For an autocomplete interaction to happen, a module + // needs to be resolved once before, then executed + const module: RootModule | SubModule = + resolveModule(commandPath).foundModule!; + // find the option that's currently getting autocompleted + const option = module.options.find(option => option.name === input.name); + // this should never be an issue, but just in case + if (option?.autocomplete === undefined) { + return; + } + // use the autocomplete function defined with the options, and return + const autocompleteValues = await option.autocomplete(input.value); + interaction.respond(autocompleteValues); +}); + // Login to discord client.login(botConfig.authToken); diff --git a/src/core/modules.ts b/src/core/modules.ts index 743cfed..aac7e54 100644 --- a/src/core/modules.ts +++ b/src/core/modules.ts @@ -8,6 +8,7 @@ import {botConfig} from './config.js'; import { APIApplicationCommandOptionChoice, APIEmbed, + ApplicationCommandOptionChoiceData, ChatInputCommandInteraction, CommandInteractionOption, } from 'discord.js'; @@ -77,14 +78,27 @@ export interface ModuleInputOption { * * Autocomplete *cannot* be set to true if you have defined choices, and this * only applies for string, integer, and number options. - * - * **THIS FUNCTIONALITY IS NOT YET IMPLEMENTED** */ choices?: APIApplicationCommandOptionChoice[]; /** - * TODO: autocomplete docstring and other thing - * https://discordjs.guide/slash-commands/autocomplete.html#responding-to-autocomplete-interactions + * If you need dynamic option generation, this is for you. + * + * This function is passed whatever the user has currently typed so far. + * It should return an array of {@link ApplicationCommandOptionChoiceData}. + * @example + * ``` + * // this very basic example takes the input, and searches an array for matches + * const options = ['foo', 'bar', 'bat'] + * const autocompleteFunction = input => + * // this oneliner removes any options + * options.filter(option => option.startsWith(value)).map(option => {name: option, value: option}); + * ``` + * + * @doc https://discordjs.guide/slash-commands/autocomplete.html#responding-to-autocomplete-interactions */ + autocomplete?: ( + currentlyTyped: string + ) => Promise; } interface ModuleConfig { diff --git a/src/core/slash_commands.ts b/src/core/slash_commands.ts index 359323c..ad2277e 100644 --- a/src/core/slash_commands.ts +++ b/src/core/slash_commands.ts @@ -254,7 +254,6 @@ function setOptionFieldsForCommand( .setDescription(setFromModuleOption.description) .setRequired(setFromModuleOption.required ?? false); if ( - setFromModuleOption.choices !== undefined && [ ModuleOptionType.Integer, ModuleOptionType.Number, @@ -262,9 +261,14 @@ function setOptionFieldsForCommand( ].includes(setFromModuleOption.type) ) { // this could be integer, number, or string - (option as SlashCommandStringOption).addChoices( - ...(setFromModuleOption.choices! as APIApplicationCommandOptionChoice[]) - ); + if (setFromModuleOption.choices !== undefined) { + (option as SlashCommandStringOption).addChoices( + ...(setFromModuleOption.choices! as APIApplicationCommandOptionChoice[]) + ); + } + if (setFromModuleOption.autocomplete !== undefined) { + (option as SlashCommandStringOption).setAutocomplete(true); + } } return option; } diff --git a/src/modules/factoids/factoids.ts b/src/modules/factoids/factoids.ts index 8f2dbc5..0e42fe0 100644 --- a/src/modules/factoids/factoids.ts +++ b/src/modules/factoids/factoids.ts @@ -10,6 +10,7 @@ import {request} from 'undici'; import * as util from '../../core/util.js'; import { + ApplicationCommandOptionChoiceData, Attachment, BaseMessageOptions, ChatInputCommandInteraction, @@ -158,7 +159,7 @@ class FactoidCache { }); // return true or false depending on whether or not a factoid was deleted if (deletionResult.deletedCount === 1) { - // The factoid was succesfully deleted + // The factoid was successfully deleted return true; } // Nothing got deleted @@ -213,6 +214,22 @@ factoid.onInitialize(async () => { }); }); +/** This function is used for slash command option autocomplete */ +async function factoidAutocomplete( + input: string +): Promise { + // https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-case-insensitive-regular-expression-match + const matches = factoidCache!.factoidCollection.find({ + triggers: {$regex: `(${input})(.*)`}, + }); + + // format results in the appropriate autocomplete format + const formattedMatches = await matches + .map(factoid => ({name: factoid.triggers[0], value: factoid.triggers[0]})) + .toArray(); + return formattedMatches; +} + /** * @param interaction The interaction to send the confirmation to * @param factoidName The factoid name to confirm the deletion of @@ -247,6 +264,7 @@ factoid.registerSubModule( name: 'factoid', description: 'The factoid to fetch', required: true, + autocomplete: factoidAutocomplete, }, ], async (args, interaction) => { @@ -410,6 +428,7 @@ factoid.registerSubModule( name: 'factoid', description: 'The factoid to forget', required: true, + autocomplete: factoidAutocomplete, }, ], async (args, interaction) => { @@ -455,6 +474,7 @@ factoid.registerSubModule( name: 'factoid', description: 'The factoid to fetch the json of', required: true, + autocomplete: factoidAutocomplete, }, ], async (args, interaction) => { @@ -472,7 +492,7 @@ factoid.registerSubModule( } // Converts the JSON contents to a buffer so it can be sent as an attachment - const serializedFactoid = JSON.stringify(locatedFactoid); + const serializedFactoid = JSON.stringify(locatedFactoid.message); const files = Buffer.from(serializedFactoid); await util @@ -504,6 +524,7 @@ trigger.registerSubmodule( name: 'factoid', description: 'The factoid to add the trigger to', required: true, + autocomplete: factoidAutocomplete, }, { type: util.ModuleOptionType.String, @@ -598,6 +619,7 @@ trigger.registerSubmodule( name: 'trigger', description: 'The trigger to remove', required: true, + autocomplete: factoidAutocomplete, }, ], async args => {