UNPKG

@ayanaware/bentocord

Version:

Bentocord is a Bento plugin designed to rapidly build fully functional Discord Bots.

282 lines 15.1 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HelpManager = void 0; const bento_1 = require("@ayanaware/bento"); const BentocordInterface_1 = require("../BentocordInterface"); const LocalizedEmbedBuilder_1 = require("../builders/LocalizedEmbedBuilder"); const ComponentOperation_1 = require("../components/ComponentOperation"); const Select_1 = require("../components/helpers/Select"); const CodeblockPaginator_1 = require("../prompt/helpers/CodeblockPaginator"); const CommandManager_1 = require("./CommandManager"); const OptionType_1 = require("./constants/OptionType"); class HelpManager { constructor() { this.name = '@ayanaware/bentocord:HelpManager'; this.parent = CommandManager_1.CommandManager; this.definition = { name: ['help', { key: 'BENTOCORD_COMMAND_HELP' }], description: { key: 'BENTOCORD_COMMAND_HELP_DESCRIPTION', backup: 'Learn about commands and features the bot provides.' }, options: [ { type: OptionType_1.OptionType.STRING, name: 'input', description: { key: 'BENTOCORD_OPTION_HELP_INPUT_DESCRIPTION', backup: 'Category, Command, or Page' }, rest: true, required: false }, ], }; } async execute(ctx, { input }) { if (!input) return this.showPrimaryHelp(ctx); const args = input.split(' '); // first argument is command or category name const name = args.shift().toLocaleLowerCase(); // handle commands const command = this.cm.findCommand(name); if (command) { return this.showCommandHelp(ctx, command.definition, args); } // handle categories for (const [category, commands] of this.cm.getCategorizedCommands()) { if (category.toLocaleLowerCase() !== name) continue; return this.showCategoryHelp(ctx, category, commands); } return ctx.createTranslatedResponse('BENTOCORD_HELP_NOT_FOUND', {}, 'Failed to find relevant help for input'); } async showPrimaryHelp(ctx) { // grab embed let embed = new LocalizedEmbedBuilder_1.LocalizedEmbedBuilder(ctx); embed = await this.interface.getHelpEmbed(embed); // apply fields const unsorted = []; const categories = this.cm.getCategorizedCommands(); for (const [category, commands] of categories) { const names = Array.from(commands.entries()) .filter(([, v]) => !v.definition.hidden) // remove hidden .map(k => k[0]).sort(); const backup = category.charAt(0).toUpperCase() + category.slice(1); const display = await ctx.formatTranslation(`HELP_CATEGORY_${category.toLocaleUpperCase()}`, {}, backup); unsorted.push([category, display, names]); } // sort alphabetically and addFields unsorted.sort().forEach(([, display, names]) => embed.addField(display, `\`${names.join('`, `')}\``, false)); // add help usage details await embed.addTranslatedField('\u200b', { key: 'BENTOCORD_HELP_USAGE', backup: [ '`help commandName` - Command Details', '`help commandName subCommandName` - Sub Command Details', '`help commandName optionName` - Option Details', '`help categoryName` - Category Details', ].join('\n') }); const sltCategory = await new Select_1.Select(ctx, 'bc:help:category', async (slt) => { await slt.deferUpdate(); await op.close(); return this.showCategoryHelp(ctx, slt.values[0]); }).addOptions(unsorted.map(([value, label]) => ({ value, label }))) .placeholderTranslated('BENTOCORD_HELP_SELECT_CATEGORY', null, 'Select a Category'); const op = new ComponentOperation_1.ComponentOperation(ctx) .content({ content: '', embeds: [embed] }) .addRows([[sltCategory]]); return op.start(); } async showCategoryHelp(ctx, category, commands) { if (!commands) commands = this.cm.getCategorizedCommands().get(category); const choices = []; for (const [command, details] of Array.from(commands.entries()).sort()) { // skip hidden if (details.definition.hidden ?? false) continue; const description = details.definition.description; const label = command; choices.push({ label, description, value: details.definition, match: [command] }); } const backup = category.charAt(0).toUpperCase() + category.slice(1); const embed = new LocalizedEmbedBuilder_1.LocalizedEmbedBuilder(ctx); await embed.setTranslatedTitle({ key: `BENTOCORD_HELP_CATEGORY_${category.toLocaleUpperCase()}_DESCRIPTION`, backup }); const choice = await ctx.choice(new (class extends CodeblockPaginator_1.CodeblockPaginator { async render() { embed.setDescription((await this.build()).render()); return { content: '', embeds: [embed] }; } })(ctx, choices), null, { showCloseError: false }); if (choice) return this.showCommandHelp(ctx, choice); } // TODO: refactor this mess to be more maintainable async showCommandHelp(ctx, definition, path = []) { let selected = definition; let selectedPath = []; const list = new Map(); // Good god what is this mess. Someone please fix // This function is responsible for fully recursing down a command object // This allows us to get a flat object with key = full command path, value = description // TODO: use this.cm.getItemTranslations & support localized help names const walkCommand = (item, depth = 0, crumb = []) => { // append ourself to crumb crumb = [...crumb, this.cm.getPrimaryName(item.name)]; // process options recursivly const filter = path[depth] ?? null; let found = !filter; for (const option of item.options ?? []) { const name = this.cm.getPrimaryName(option.name); // filter when requested, keep track of "selected" item if (filter) { if (filter !== name) continue; selected = option; selectedPath = [...crumb]; found = true; } // recurse & continue if options are available if (!this.cm.isAnySubCommand(option)) continue; // skip hidden, unless it was filtered if (!filter && (option.hidden ?? false)) continue; // no subcommand/group children, so add to list if (!(option.options ?? []).some(o => this.cm.isAnySubCommand(o))) { // don't add to list if filter selected this specific item, this lets showCommandHelp know to not show subCommand dialog if (!filter) list.set([...crumb, name].join(' '), option.description); } // TIL: ++x = value after increment, x++ = value before increment if (!walkCommand(option, ++depth, crumb)) return false; } return found; }; if (!walkCommand(definition)) return ctx.createTranslatedResponse('BENTOCORD_HELP_NOT_FOUND', {}, 'Failed to find any relevant help for input.'); // selected = the command/option that was requested // list = a map of all valid sub commands selected = selected; // fix types for some reason let description = selected.description; if (typeof description === 'object') description = await ctx.formatTranslation(description.key, description.repl, description.backup); // if there are items in list then show a choice prompt with them // allows users to drill down futher if they want if (list.size > 0) { // translate descriptions const choices = Array.from(list.entries()).map(([label, desc]) => ({ label, description: desc, value: label.split(' '), match: [label], })); const getEmbed = async () => this.buildCommandEmbed(ctx, selected, selectedPath); const choice = await ctx.choice(new (class extends CodeblockPaginator_1.CodeblockPaginator { async render() { const embed = await getEmbed(); await embed.addTranslatedField({ key: 'BENTOCORD_WORD_SUBCOMMANDS', backup: 'Sub Commands' }, (await this.build()).render()); return { content: '', embeds: [embed] }; } })(ctx, choices), null, { showCloseError: false }); // user picked something remove first element and invoke ourself again if (choice) return this.showCommandHelp(ctx, definition, choice.slice(1)); return; } // selected is now either a option, or a command/subcommand/group with no child subcommand/group // handle options if ('type' in selected && !this.cm.isAnySubCommand(selected)) return this.displayOption(ctx, selected, selectedPath); // handle command/subcommand/group return this.displayCommand(ctx, definition, selected, selectedPath); } async buildCommandEmbed(ctx, command, crumb) { const primary = this.cm.getPrimaryName(command.name); const full = [...crumb, primary]; const embed = await new LocalizedEmbedBuilder_1.LocalizedEmbedBuilder(ctx) .setTitle(full.join(' ')) .setTranslatedDescription(command.description); // aliases const names = await this.cm.getItemTranslations(command.name); const aliases = names.map(n => n[0]).filter(n => n !== primary); if (aliases.length > 0) { await embed.addTranslatedField({ key: 'BENTOCORD_WORD_ALIASES', backup: 'Aliases' }, aliases.map(a => `\`${a}\``).join(', ')); } // TODO: Dynamically generate examples and/or pull from definition return embed; } async displayCommand(ctx, definition, command, crumb = []) { const getEmbed = async () => this.buildCommandEmbed(ctx, command, crumb); const primary = this.cm.getPrimaryName(command.name); const options = command.options ?? []; if (options.length === 0) return ctx.createResponse({ content: '', embeds: [await getEmbed()] }); const choices = []; for (const option of options) { if (this.cm.isAnySubCommand(option)) continue; const name = this.cm.getPrimaryName(option.name); const type = this.cm.getTypePreview(option); let description = option.description; if (typeof description === 'object') description = await ctx.formatTranslation(description); choices.push({ label: `${name}${type}`, description, value: name, match: [name] }); } const choice = await ctx.choice(new (class extends CodeblockPaginator_1.CodeblockPaginator { async render() { const embed = await getEmbed(); await embed.addTranslatedField({ key: 'BENTOCORD_WORD_OPTIONS', backup: 'Options' }, (await this.build()).render()); return { content: '', embeds: [embed] }; } })(ctx, choices), null, { showCloseError: false }); if (choice) return this.showCommandHelp(ctx, definition, [...crumb, primary, choice].slice(1)); } async displayOption(ctx, option, crumb) { const primary = this.cm.getPrimaryName(option.name); const getEmbed = async () => { const embed = await new LocalizedEmbedBuilder_1.LocalizedEmbedBuilder(ctx) .setTitle(primary) .setAuthor(crumb.join(' ')) .setTranslatedDescription(option.description); const type = this.cm.getTypePreview(option); await embed.addTranslatedField({ key: 'BENTOCORD_WORD_TYPE', backup: 'Type' }, type, true); const required = option.required ?? true; await embed.addTranslatedField({ key: 'BENTOCORD_WORD_REQUIRED', backup: 'Required' }, required ? '✅' : '❌', true); // Add option help info const resolver = this.cm.findResolver(option.type); if (resolver && typeof resolver.help === 'function') { try { const help = await resolver.help(ctx, option, new Map()); for (const [key, value] of help.entries()) embed.addField(key, value, true); } catch { /* no op */ } } return embed; }; // handle choices if ('choices' in option) { let choices = option.choices; if (typeof choices === 'function') choices = await choices(); return ctx.pagination(new (class extends CodeblockPaginator_1.CodeblockPaginator { async render() { const embed = await getEmbed(); await embed.addTranslatedField({ key: 'BENTOCORD_WORD_CHOICES', backup: 'Choices' }, (await this.build()).render()); return { content: '', embeds: [embed] }; } })(ctx, choices.map(c => ({ label: c.value.toString(), description: c.description }))), null, { showCloseError: false }); } // TODO: Dynamically generate example values based on type return ctx.createResponse({ content: '', embeds: [await getEmbed()] }); } } __decorate([ (0, bento_1.Inject)(), __metadata("design:type", BentocordInterface_1.BentocordInterface) ], HelpManager.prototype, "interface", void 0); __decorate([ (0, bento_1.Inject)(), __metadata("design:type", CommandManager_1.CommandManager) ], HelpManager.prototype, "cm", void 0); exports.HelpManager = HelpManager; //# sourceMappingURL=HelpManager.js.map