diff --git a/src/commands/loader.ts b/src/commands/loader.ts index c606616..a342ac8 100644 --- a/src/commands/loader.ts +++ b/src/commands/loader.ts @@ -2,6 +2,7 @@ import { REST } from '@discordjs/rest'; import { Routes } from 'discord-api-types/v9'; import { Client } from 'discord.js'; import { readdir } from 'fs/promises'; +import { removeExtension } from '../utils/misc'; /** Load all the commands. */ export default async (client: Client) => { @@ -17,6 +18,12 @@ export default async (client: Client) => { // Retrieve all the commands const command_files = await readdir(`${__dirname}/${command_category}`); + // Add the category to the collection for the help command + client.commands.categories.set( + command_category, + command_files.map(removeExtension), + ); + // Add the command return Promise.all( command_files.map(async command_file => { @@ -25,9 +32,10 @@ export default async (client: Client) => { ).default; // Add it to the collection so the interaction will work - client.commands.set(command.data(client).name, command); + command.data = command.data(client); + client.commands.list.set(command.data.name, command); - return command.data(client).toJSON(); + return command.data.toJSON(); }), ); }), diff --git a/src/commands/misc/help.ts b/src/commands/misc/help.ts new file mode 100644 index 0000000..2bfca41 --- /dev/null +++ b/src/commands/misc/help.ts @@ -0,0 +1,94 @@ +import { SlashCommandBuilder } from '@discordjs/builders'; +import { Locale } from 'discord-api-types/v9'; +import { Client, CommandInteraction, MessageEmbed } from 'discord.js'; +import { getLocale, getLocalizations } from '../../utils/locales'; +import { getFilename } from '../../utils/misc'; +import '../../modules/string'; + +export default { + data: (client: Client) => { + const filename = getFilename(__filename); + return new SlashCommandBuilder() + .setName( + filename.toLowerCase()) + .setDescription(client.locales.get(client.config.default_lang) + ?.get(`c_${filename}_desc`) ?? '') + .setNameLocalizations( + getLocalizations(client, `c_${filename}_name`, true)) + .setDescriptionLocalizations( + getLocalizations(client, `c_${filename}_desc`)) + + // Command option + .addStringOption(option => option + .setName(client.locales.get(client.config.default_lang) + ?.get(`c_${filename}_opt1_name`) ?? '') + .setDescription(client.locales.get(client.config.default_lang) + ?.get(`c_${filename}_opt1_desc`) ?? '') + .setNameLocalizations( + getLocalizations(client, `c_${filename}_opt1_name`, true)) + .setDescriptionLocalizations( + getLocalizations(client, `c_${filename}_opt1_desc`)) + ); + }, + + interaction: async (interaction: CommandInteraction, client: Client) => { + const loc = getLocale(client, interaction.locale); + const desired_command = interaction.options.getString(client + .locales + .get(client.config.default_lang) + ?.get(`c_${getFilename(__filename)}_opt1_name`) ?? ''); + + // If a command isn't specified + if (!desired_command) { + const fields: { + name: string; + value: string; + }[] = []; + + // Load all the command per categories + client.commands.categories.forEach((commands_name, category) => { + const commands = commands_name.reduce((data, command_name) => { + return data + `\`/${command_name}\`, `; + }, ''); + + fields.push({ + name: category.capitalize() + ` (${commands_name.length})`, + value: commands.slice(0, -2), + }); + }); + + // Sends a list of commands sorted into categories + return interaction.reply({ embeds: [ + new MessageEmbed() + .setColor('BLURPLE') + .setTitle(loc.get('c_help1')) + .setDescription(loc.get('c_help2')) + .addFields(fields), + ] }); + } + + // If a command is specified + const command = client.commands.list.get(desired_command); + if (!command) { + // Command don't exist + return interaction.reply({ + content: `${loc.get('c_help3')} \`${desired_command}\``, + ephemeral: true, + }); + } + + // Send information about the command + return interaction.reply({ embeds: [ + new MessageEmbed() + .setColor('BLURPLE') + .setTitle('`/' + command.data.name + '`') + .setDescription( + // Loads the description + // according to the user's locals + command.data.description_localizations + ?.[interaction.locale as Locale] + ?? command.data.description + ), + ] }); + }, +}; diff --git a/src/commands/misc/ping.ts b/src/commands/misc/ping.ts index 26b586e..9f56312 100644 --- a/src/commands/misc/ping.ts +++ b/src/commands/misc/ping.ts @@ -7,16 +7,24 @@ export default { data: (client: Client) => { const filename = getFilename(__filename); return new SlashCommandBuilder() - .setName(filename.toLowerCase()) - .setDescription(client.locales.get(client.config.default_lang)?.get(`c_${filename}_desc`) ?? '?') - .setNameLocalizations(getLocalizations(client, `c_${filename}_name`)) - .setDescriptionLocalizations(getLocalizations(client, `c_${filename}_desc`)); + .setName( + filename.toLowerCase()) + .setDescription(client.locales.get(client.config.default_lang) + ?.get(`c_${filename}_desc`) ?? '') + .setNameLocalizations( + getLocalizations(client, `c_${filename}_name`, true)) + .setDescriptionLocalizations( + getLocalizations(client, `c_${filename}_desc`) + ); }, interaction: async (interaction: CommandInteraction, client: Client) => { const loc = getLocale(client, interaction.locale); - const sent = await interaction.reply({ content: 'Pinging...', fetchReply: true }) as Message; + const sent = await interaction.reply({ + content: 'Pinging...', + fetchReply: true, + }) as Message; interaction.editReply( `${loc?.get('c_ping1')}: \ diff --git a/src/events/interactions/interactionCreate.ts b/src/events/interactions/interactionCreate.ts index 525a29a..f4b57bd 100644 --- a/src/events/interactions/interactionCreate.ts +++ b/src/events/interactions/interactionCreate.ts @@ -4,7 +4,7 @@ import { getLocale } from '../../utils/locales'; /** https://discord.js.org/#/docs/discord.js/main/class/Client?scrollTo=e-interactionCreate */ export default (interaction: Interaction, client: Client) => { if (interaction.isCommand()) { - const command = client.commands.get(interaction.commandName); + const command = client.commands.list.get(interaction.commandName); if (!command) { const loc = getLocale(client, interaction.locale); return interaction.reply({ diff --git a/src/locales/en-US.json b/src/locales/en-US.json index fe704bf..6f0439a 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -1,7 +1,16 @@ { "e_interacreate_no_command": "Sorry, the command probably no longer exists...", + "c_ping_name": "Ping", "c_ping_desc": "Pong!", "c_ping1": "Roundtrip latency", - "c_ping2": "Websocket heartbeat" + "c_ping2": "Websocket heartbeat", + + "c_help_name": "Help", + "c_help_desc": "Informations about commands", + "c_help_opt1_name": "command", + "c_help_opt1_desc": "Command wanted in depth.", + "c_help1": "List of categories and associated commands", + "c_help2": "`/help ` to get more information about a command.", + "c_help3": "Can't find :" } diff --git a/src/locales/fr.json b/src/locales/fr.json index 24f0dbe..cad1fba 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -1,7 +1,16 @@ { "e_interacreate_no_command": "Désolé, la commande n'existe plus...", + "c_ping_name": "Ping", "c_ping_desc": "Pong!", "c_ping1": "Latence totale", - "c_ping2": "Latence du Websocket" + "c_ping2": "Latence du Websocket", + + "c_help_name": "Aide", + "c_help_desc": "Informations sur les commandes", + "c_help_opt1_name": "commande", + "c_help_opt1_desc": "Commande voulu en détail.", + "c_help1": "Liste des catégories et des commandes associées", + "c_help2": "`/help ` pour obtenir plus d'informations sur une commande.", + "c_help3": "Impossible de trouver :" } diff --git a/src/modules/string.ts b/src/modules/string.ts new file mode 100644 index 0000000..01bbe12 --- /dev/null +++ b/src/modules/string.ts @@ -0,0 +1,16 @@ +export {}; + +declare global { + // Declarations + interface String { + /** + * Returns a copy of the string with the first letter capitalized. + */ + capitalize(): string, + } +} + +/** Capitalize definition */ +String.prototype.capitalize = function(this: string) { + return this[0].toUpperCase() + this.substring(1); +}; diff --git a/src/utils/client.ts b/src/utils/client.ts index 85baa4e..2176192 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -16,13 +16,25 @@ declare module 'discord.js' { default_lang: string }, /** Store all the slash commands */ - commands: Collection< - string, - { - data: SlashCommandBuilder, - interaction: (interaction: CommandInteraction, client: Client) => unknown - } - >, + commands: { + categories: Collection< + /** Category name */ + string, + /** Name of the commands in the category */ + string[] + >, + list: Collection< + /** Command name */ + string, + /** Command itself */ + { + /** Data about the command */ + data: SlashCommandBuilder, + /** How the command interact */ + interaction: (interaction: CommandInteraction, client: Client) => unknown + } + >, + } /** Store all the localizations */ locales: Map> } @@ -42,7 +54,10 @@ export default async () => { default_lang: process.env.DEFAULT_LANG ?? 'fr', }; - client.commands = new Collection(); + client.commands = { + categories: new Collection(), + list: new Collection(), + }; console.log('Translations progression :'); client.locales = await loadLocales(client.config.default_lang); diff --git a/src/utils/locales.ts b/src/utils/locales.ts index c82f291..12c0b84 100644 --- a/src/utils/locales.ts +++ b/src/utils/locales.ts @@ -53,18 +53,21 @@ export const loadLocales = async (default_lang: string) => { * @param text Name of string to fetch * @returns the dictionary */ -export const getLocalizations = (client: Client, text: string) => { +export const getLocalizations = (client: Client, text: string, lowercase = false) => { const data: Record = {}; // Load all the localizations client.locales.forEach((locale, lang) => { // Fetch the text and fallback to default lang if needed // See getLocale for more info on why we *can* fallback - const str = locale.get(text) + let str = locale.get(text) ?? client.locales.get(client.config.default_lang)?.get(text); // Store it if defined if (str !== undefined) { + if (lowercase) { + str = str.toLowerCase(); + } data[lang] = str; } });