@ayanaware/bentocord
Version:
Bentocord is a Bento plugin designed to rapidly build fully functional Discord Bots.
282 lines • 15.1 kB
JavaScript
;
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