From 34b55d206c5048b4abacf68c663261f494c888a2 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Thu, 2 Jun 2022 18:09:51 -0400 Subject: feat(helpCommand): add arguments --- src/commands/dev/superUser.ts | 6 +- src/commands/info/help.ts | 142 +++++++++++++++++------ src/commands/moderation/ban.ts | 2 + src/commands/utilities/steal.ts | 4 +- src/lib/extensions/discord-akairo/BushCommand.ts | 134 +++++++++++++++------ 5 files changed, 209 insertions(+), 79 deletions(-) diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index 57e2b86..d62ac8e 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -15,12 +15,12 @@ export default class SuperUserCommand extends BushCommand { ownerOnly: true, helpArgs: [ { - id: 'action', + name: 'action', description: 'Whether to add or remove a user from the superuser list.', - readableType: 'add|remove' + type: "'add'|'remove'" }, { - id: 'user', + name: 'user', description: 'The user to add/remove from the superuser list.', type: 'user', match: 'restContent' diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 7a30e64..ea1e965 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -11,6 +11,7 @@ import { } from 'discord.js'; import Fuse from 'fuse.js'; import packageDotJSON from '../../../package.json' assert { type: 'json' }; +import { stripIndent } from '../../lib/common/tags.js'; assert(Fuse); assert(packageDotJSON); @@ -52,51 +53,71 @@ export default class HelpCommand extends BushCommand { }); } - public override async exec( - message: BushMessage | BushSlashMessage, - args: { command: ArgType<'commandAlias'> | string; showHidden?: boolean } - ) { - const prefix = util.prefix(message); + public override async exec(message: BushMessage | BushSlashMessage, args: HelpArgs) { const row = this.addLinks(message); - - const isOwner = client.isOwner(message.author); - const isSuperUser = client.isSuperUser(message.author); const command = args.command ? typeof args.command === 'string' ? client.commandHandler.findCommand(args.command) ?? null : args.command : null; - if (!isOwner) args.showHidden = false; + + if (!message.author.isOwner()) args.showHidden = false; + if (!command || command.pseudo) { - const embed = new EmbedBuilder().setColor(util.colors.default).setTimestamp(); - embed.setFooter({ text: `For more information about a command use ${prefix}help ` }); - for (const [, category] of this.handler.categories) { - const categoryFilter = category.filter((command) => { - if (command.pseudo) return false; - if (command.hidden && !args.showHidden) return false; - if (command.channel == 'guild' && !message.guild && !args.showHidden) return false; - if (command.ownerOnly && !isOwner) return false; - if (command.superUserOnly && !isSuperUser) return false; - if (command.restrictedGuilds?.includes(message.guild?.id ?? '') === false && !args.showHidden) return false; - if (command.aliases.length === 0) return false; - - return true; - }); - const categoryNice = category.id - .replace(/(\b\w)/gi, (lc) => lc.toUpperCase()) - .replace(/'(S)/g, (letter) => letter.toLowerCase()); - const categoryCommands = categoryFilter.filter((cmd) => cmd.aliases.length > 0).map((cmd) => `\`${cmd.aliases[0]}\``); - if (categoryCommands.length > 0) { - embed.addFields([{ name: `${categoryNice}`, value: `${categoryCommands.join(' ')}` }]); - } - } - return await message.util.reply({ embeds: [embed], components: row.components.length ? [row] : undefined }); + return this.helpAll(message, args, row); + } else { + return this.helpIndividual(message, row, command); } + } + private helpAll(message: BushMessage | BushSlashMessage, args: HelpArgs, row: ActionRowBuilder) { + const prefix = util.prefix(message); const embed = new EmbedBuilder() .setColor(util.colors.default) - .setTitle(`${command.id} Command`) - .setDescription(`${command.description ?? '*This command does not have a description.*'}`); + .setTimestamp() + .setFooter({ text: `For more information about a command use ${prefix}help ` }); + for (const [, category] of this.handler.categories) { + const categoryFilter = category.filter((command) => { + if (command.pseudo) return false; + if (command.hidden && !args.showHidden) return false; + if (command.channel == 'guild' && !message.guild && !args.showHidden) return false; + if (command.ownerOnly && !message.author.isOwner()) return false; + if (command.superUserOnly && !message.author.isSuperUser()) return false; + if (command.restrictedGuilds?.includes(message.guild?.id ?? '') === false && !args.showHidden) return false; + if (command.aliases.length === 0) return false; + + return true; + }); + const categoryNice = category.id + .replace(/(\b\w)/gi, (lc) => lc.toUpperCase()) + .replace(/'(S)/g, (letter) => letter.toLowerCase()); + const categoryCommands = categoryFilter.filter((cmd) => cmd.aliases.length > 0).map((cmd) => `\`${cmd.aliases[0]}\``); + if (categoryCommands.length > 0) { + embed.addFields([{ name: `${categoryNice}`, value: `${categoryCommands.join(' ')}` }]); + } + } + return message.util.reply({ embeds: [embed], components: row.components.length ? [row] : undefined }); + } + + private helpIndividual(message: BushMessage | BushSlashMessage, row: ActionRowBuilder, command: BushCommand) { + const embed = new EmbedBuilder().setColor(util.colors.default).setTitle(`${command.id} Command`); + + let description = `${command.description ?? '*This command does not have a description.*'}`; + if (command.note) description += `\n\n${command.note}`; + embed.setDescription(description); + + this.addCommandUsage(embed, command); + this.addCommandExamples(embed, command); + this.addCommandAliases(embed, command); + this.addCommandArguments(embed, command, message.author.isOwner(), message.author.isSuperUser()); + this.addCommandRestrictions(embed, command); + // todo: permissions + + const params = { embeds: [embed], components: row.components.length ? [row] : undefined }; + return message.util.reply(params); + } + + private addCommandUsage(embed: EmbedBuilder, command: BushCommand) { if (command.usage?.length) { embed.addFields([ { @@ -105,6 +126,9 @@ export default class HelpCommand extends BushCommand { } ]); } + } + + private addCommandExamples(embed: EmbedBuilder, command: BushCommand) { if (command.examples?.length) { embed.addFields([ { @@ -113,7 +137,50 @@ export default class HelpCommand extends BushCommand { } ]); } - if (command.aliases?.length > 1) embed.addFields([{ name: '» Aliases', value: `\`${command.aliases.join('` `')}\`` }]); + } + + private addCommandAliases(embed: EmbedBuilder, command: BushCommand) { + if (command.aliases?.length > 1) + embed.addFields([ + { + name: '» Aliases', + value: `\`${command.aliases.join('` `')}\`` + } + ]); + } + + private addCommandArguments(embed: EmbedBuilder, command: BushCommand, isOwner = false, isSuperUser = false) { + const format = (id: string, req: boolean) => `${req ? '<' : '['}${id}${req ? '>' : ']'}`; + const args = (command.argsInfo ?? []).filter((arg) => { + if (arg.ownerOnly && !isOwner) return false; + if (arg.superUserOnly && !isSuperUser) return false; + return true; + }); + if (args.length) { + embed.addFields([ + { + name: '» Arguments', + value: args + .map((a) => { + let ret = stripIndent` + \`${format(a.name, !!a.optional)}\` + ⠀‣ **Desc**: ${a.description} + ⠀‣ **Type**: ${typeof a.type !== 'function' ? a.type : '[no readable type]'}`; + + if (a.flag?.length) ret += `\n⠀‣ **Flags**: ${a.flag.map((f) => `"${f}"`).join(', ')}`; + ret += `\n⠀‣ **Kind**: ${a.only ?? 'text & slash'}`; + if (a.only !== 'slash') ret += `\n⠀‣ **Match**: ${a.match}`; + if (a.only !== 'text') ret += `\n⠀‣ **Autocomplete**: ${a.autocomplete}`; + + return ret; + }) + .join('\n') + } + ]); + } + } + + private addCommandRestrictions(embed: EmbedBuilder, command: BushCommand) { if ( command.ownerOnly || command.superUserOnly || @@ -138,9 +205,6 @@ export default class HelpCommand extends BushCommand { ); if (restrictions.length) embed.addFields([{ name: '» Restrictions', value: restrictions.join('\n') }]); } - - const params = { embeds: [embed], components: row.components.length ? [row] : undefined }; - return await message.util.reply(params); } private addLinks(message: BushMessage | BushSlashMessage) { @@ -181,3 +245,5 @@ export default class HelpCommand extends BushCommand { void interaction.respond(res.length ? res : startingCommands); } } + +type HelpArgs = { command: ArgType<'commandAlias'> | string; showHidden?: boolean }; diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 14bbba6..77951c3 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -24,6 +24,7 @@ export default class BanCommand extends BushCommand { id: 'user', description: 'The user that will be banned.', type: util.arg.union('user', 'snowflake'), + readableType: 'user|snowflake', prompt: 'What user would you like to ban?', retry: '{error} Choose a valid user to ban.', slashType: ApplicationCommandOptionType.User @@ -46,6 +47,7 @@ export default class BanCommand extends BushCommand { prompt: "How many days of the user's messages would you like to delete?", retry: '{error} Choose between 0 and 7 days to delete messages from the user for.', type: util.arg.range('integer', 0, 7, true), + readableType: 'integer [0, 7]', optional: true, slashType: ApplicationCommandOptionType.Integer, choices: [...Array(8).keys()].map((v) => ({ name: v.toString(), value: v })) diff --git a/src/commands/utilities/steal.ts b/src/commands/utilities/steal.ts index 765fb24..e4f08f1 100644 --- a/src/commands/utilities/steal.ts +++ b/src/commands/utilities/steal.ts @@ -31,8 +31,8 @@ export default class StealCommand extends BushCommand { { name: 'name', description: lang.nameStart, type: ApplicationCommandOptionType.String, required: false } ], helpArgs: [ - { id: 'emoji', description: lang.emojiDescription, readableType: 'emoji|emojiId|url', optional: false }, - { id: 'name', description: lang.nameDescription, readableType: 'string', optional: true } + { name: 'emoji', description: lang.emojiDescription, type: 'emoji|emojiId|url', optional: false }, + { name: 'name', description: lang.nameDescription, type: 'string', optional: true } ], slash: true, channel: 'guild', diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 1797be8..c727e98 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -26,6 +26,7 @@ import { type ParsedDuration } from '#lib'; import { + ArgumentMatch, Command, type AkairoApplicationCommandAutocompleteOption, type AkairoApplicationCommandChannelOptionData, @@ -53,6 +54,7 @@ import { type PermissionsString, type Snowflake } from 'discord.js'; +import _ from 'lodash'; export interface OverriddenBaseArgumentType extends BaseArgumentType { user: BushUser | null; @@ -113,7 +115,7 @@ export interface BaseBushArgumentType extends OverriddenBaseArgumentType { export type BushArgumentType = keyof BaseBushArgumentType | RegExp; -interface BaseBushArgumentOptions extends Omit { +interface BaseBushArgumentOptions extends Omit, ExtraArgumentOptions { id: string; description: string; @@ -168,7 +170,9 @@ interface BaseBushArgumentOptions extends Omit & { slashType?: AkairoApplicationCommandOptionData['type'] })[]; + helpArgs?: ArgsInfo[]; /** * Extra information about the command, displayed in the help command. @@ -318,12 +334,12 @@ export interface BaseBushCommandOptions userPermissions: bigint | bigint[] | BushMissingPermissionSupplier; /** - * Restrict this argument to owners + * Whether the argument is only accessible to the owners. */ ownerOnly?: boolean; /** - * Restrict this argument to super users. + * Whether the argument is only accessible to the super users. */ superUserOnly?: boolean; } @@ -331,30 +347,62 @@ export interface BaseBushCommandOptions export type BushCommandOptions = Omit | Omit; export interface ArgsInfo { - id: string; - description: string; - optional?: boolean; - slashType: AkairoApplicationCommandOptionData['type'] | false; - slashResolve?: SlashResolveType; - only?: 'slash' | 'text'; - type: string; -} + /** + * The name of the argument. + */ + name: string; -export interface ArgsInfoText { - id: string; + /** + * The description of the argument. + */ description: string; + + /** + * Whether the argument is optional. + * @default false + */ optional?: boolean; - only: 'text'; + + /** + * Whether or not the argument has autocomplete enabled. + * @default false + */ + autocomplete?: boolean; + + /** + * Whether the argument is restricted a certain command. + * @default 'slash & text' + */ + only?: 'slash & text' | 'slash' | 'text'; + + /** + * The method that arguments are matched for text commands. + * @default 'phrase' + */ + match?: ArgumentMatch; + + /** + * The readable type of the argument. + */ type: string; -} -export interface ArgsInfoSlash { - id: string; - description: string; - optional?: boolean; - slashType: AkairoApplicationCommandOptionData['type'] | false; - slashResolve?: SlashResolveType; - only: 'slash'; + /** + * If {@link match} is 'flag' or 'option', these are the flags that are matched + * @default [] + */ + flag?: string[]; + + /** + * Whether the argument is only accessible to the owners. + * @default false + */ + ownerOnly?: boolean; + + /** + * Whether the argument is only accessible to the super users. + * @default false + */ + superUserOnly?: boolean; } export class BushCommand extends Command { @@ -435,11 +483,11 @@ export class BushCommand extends Command { for (const _key in options_) { const key = _key as keyof typeof options_; // you got to love typescript if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') { - const newTextArgs: ArgumentOptions[] = []; + const newTextArgs: (ArgumentOptions & ExtraArgumentOptions)[] = []; const newSlashArgs: SlashOption[] = []; for (const arg of options_.args) { if (arg.only !== 'slash' && !options_.slashOnly) { - const newArg: ArgumentOptions = {}; + const newArg: ArgumentOptions & ExtraArgumentOptions = {}; if ('default' in arg) newArg.default = arg.default; if ('description' in arg) newArg.description = arg.description; if ('flag' in arg) newArg.flag = arg.flag; @@ -458,6 +506,8 @@ export class BushCommand extends Command { } if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster; if ('unordered' in arg) newArg.unordered = arg.unordered; + if ('ownerOnly' in arg) newArg.ownerOnly = arg.ownerOnly; + if ('superUserOnly' in arg) newArg.superUserOnly = arg.superUserOnly; newTextArgs.push(newArg); } if ( @@ -495,19 +545,31 @@ export class BushCommand extends Command { super(id, newOptions); - if (options_.args || options_.helpArgs) { + if (options_.args ?? options_.helpArgs) { const argsInfo: ArgsInfo[] = []; + const combined = (options_.args ?? options_.helpArgs)!.map((arg) => { + const norm = options_.args + ? options_.args.find((_arg) => _arg.id === ('id' in arg ? arg.id : arg.name)) ?? ({} as BushArgumentOptions) + : ({} as BushArgumentOptions); + const help = options_.helpArgs + ? options_.helpArgs.find((_arg) => _arg.name === ('id' in arg ? arg.id : arg.name)) ?? ({} as ArgsInfo) + : ({} as ArgsInfo); + return { ...norm, ...help }; + }); - for (const arg of (options_.args ?? options_.helpArgs)!) { - argsInfo.push({ - id: arg.id, - description: arg.description, - optional: arg.optional, - slashType: arg.slashType!, - slashResolve: arg.slashResolve, - only: arg.only, - type: (arg.readableType ?? arg.type) as string - }); + for (const arg of combined) { + const name = _.camelCase('id' in arg ? arg.id : arg.name), + description = arg.description || '*No description provided.*', + optional = arg.optional ?? false, + autocomplete = arg.autocomplete ?? false, + only = arg.only ?? 'slash & text', + match = arg.match ?? 'phrase', + type = match === 'flag' ? 'flag' : arg.readableType ?? arg.type ?? 'string', + flag = arg.flag ? (Array.isArray(arg.flag) ? arg.flag : [arg.flag]) : [], + ownerOnly = arg.ownerOnly ?? false, + superUserOnly = arg.superUserOnly ?? false; + + argsInfo.push({ name, description, optional, autocomplete, only, match, type, flag, ownerOnly, superUserOnly }); } this.argsInfo = argsInfo; -- cgit