mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Implemented rough draft of info resolver method
This commit is contained in:
		
							parent
							
								
									2a4d08d0bc
								
							
						
					
					
						commit
						6ed4c0988f
					
				
					 4 changed files with 184 additions and 125 deletions
				
			
		|  | @ -1,9 +1,9 @@ | |||
| import Command from "../../core/command"; | ||||
| import {Command, NamedCommand} from "../../core/command"; | ||||
| import {toTitleCase} from "../../core/lib"; | ||||
| import {loadableCommands, categories} from "../../core/loader"; | ||||
| import {getPermissionName} from "../../core/permissions"; | ||||
| 
 | ||||
| export default new Command({ | ||||
| export default new NamedCommand({ | ||||
|     description: "Lists all commands. If a command is specified, their arguments are listed as well.", | ||||
|     usage: "([command, [subcommand/type], ...])", | ||||
|     aliases: ["h"], | ||||
|  | @ -16,13 +16,7 @@ export default new Command({ | |||
| 
 | ||||
|             for (const header of headers) { | ||||
|                 if (header !== "test") { | ||||
|                     const command = commands.get(header); | ||||
| 
 | ||||
|                     if (!command) | ||||
|                         return console.warn( | ||||
|                             `Command "${header}" of category "${category}" unexpectedly doesn't exist!` | ||||
|                         ); | ||||
| 
 | ||||
|                     const command = commands.get(header)!; | ||||
|                     output += `\n- \`${header}\`: ${command.description}`; | ||||
|                 } | ||||
|             } | ||||
|  | @ -32,44 +26,63 @@ export default new Command({ | |||
|     }, | ||||
|     any: new Command({ | ||||
|         async run($) { | ||||
|             // [category, commandName, command, subcommandInfo] = resolveCommandInfo();
 | ||||
|             // Setup the root command
 | ||||
|             const commands = await loadableCommands; | ||||
|             let header = $.args.shift() as string; | ||||
|             let command = commands.get(header); | ||||
|             if (!command || header === "test") return $.channel.send(`No command found by the name \`${header}\`.`); | ||||
|             if (!(command instanceof NamedCommand)) | ||||
|                 return $.channel.send(`Command is not a proper instance of NamedCommand.`); | ||||
|             if (command.name) header = command.name; | ||||
| 
 | ||||
|             // Search categories
 | ||||
|             let category = "Unknown"; | ||||
|             for (const [referenceCategory, headers] of categories) { | ||||
|                 if (headers.includes(header)) { | ||||
|                     category = toTitleCase(referenceCategory); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Gather info
 | ||||
|             const result = await command.resolveInfo($.args); | ||||
| 
 | ||||
|             if (result.type === "error") return $.channel.send(result.message); | ||||
| 
 | ||||
|             let append = ""; | ||||
|             command = result.command; | ||||
| 
 | ||||
|             if (usage === "") { | ||||
|             if (command.usage === "") { | ||||
|                 const list: string[] = []; | ||||
| 
 | ||||
|                 command.subcommands.forEach((subcmd, subtag) => { | ||||
|                     // Don't capture duplicates generated from aliases.
 | ||||
|                     if (subcmd.originalCommandName === subtag) { | ||||
|                         const customUsage = subcmd.usage ? ` ${subcmd.usage}` : ""; | ||||
|                         list.push(`- \`${header} ${subtag}${customUsage}\` - ${subcmd.description}`); | ||||
|                 for (const [tag, subcommand] of result.keyedSubcommandInfo) { | ||||
|                     const customUsage = subcommand.usage ? ` ${subcommand.usage}` : ""; | ||||
|                     list.push(`- \`${header} ${tag}${customUsage}\` - ${subcommand.description}`); | ||||
|                 } | ||||
|                 }); | ||||
| 
 | ||||
|                 const addDynamicType = (cmd: Command | null, type: string) => { | ||||
|                     if (cmd) { | ||||
|                         const customUsage = cmd.usage ? ` ${cmd.usage}` : ""; | ||||
|                         list.push(`- \`${header} <${type}>${customUsage}\` - ${cmd.description}`); | ||||
|                 for (const [type, subcommand] of result.subcommandInfo) { | ||||
|                     const customUsage = subcommand.usage ? ` ${subcommand.usage}` : ""; | ||||
|                     list.push(`- \`${header} ${type}${customUsage}\` - ${subcommand.description}`); | ||||
|                 } | ||||
|                 }; | ||||
| 
 | ||||
|                 addDynamicType(command.user, "user"); | ||||
|                 addDynamicType(command.number, "number"); | ||||
|                 addDynamicType(command.any, "any"); | ||||
| 
 | ||||
|                 append = "Usages:" + (list.length > 0 ? `\n${list.join("\n")}` : " None."); | ||||
|             } else append = `Usage: \`${header} ${usage}\``; | ||||
|             } else { | ||||
|                 append = `Usage: \`${header} ${command.usage}\``; | ||||
|             } | ||||
| 
 | ||||
|             let aliases = "N/A"; | ||||
| 
 | ||||
|             if (command instanceof NamedCommand) { | ||||
|                 const formattedAliases: string[] = []; | ||||
|                 for (const alias of command.aliases) formattedAliases.push(`\`${alias}\``); | ||||
|                 // Short circuit an empty string, in this case, if there are no aliases.
 | ||||
|             const aliases = formattedAliases.join(", ") || "None"; | ||||
|                 aliases = formattedAliases.join(", ") || "None"; | ||||
|             } | ||||
| 
 | ||||
|             $.channel.send( | ||||
|                 `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${getPermissionName( | ||||
|                     permLevel | ||||
|                 )}\` (${permLevel})\nDescription: ${command.description}\n${append}`, | ||||
|             return $.channel.send( | ||||
|                 `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${category}\`\nPermission Required: \`${getPermissionName( | ||||
|                     result.permission | ||||
|                 )}\` (${result.permission})\nDescription: ${command.description}\n${append}`, | ||||
|                 {split: true} | ||||
|             ); | ||||
|         } | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {parseVars, requireAllCasesHandledFor} from "./lib"; | ||||
| import {parseVars} from "./lib"; | ||||
| import {Collection} from "discord.js"; | ||||
| import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember, GuildChannel} from "discord.js"; | ||||
| import {getPrefix} from "../core/structures"; | ||||
|  | @ -82,6 +82,36 @@ interface ExecuteCommandMetadata { | |||
|     channelType: CHANNEL_TYPE; | ||||
| } | ||||
| 
 | ||||
| interface CommandInfo { | ||||
|     readonly type: "info"; | ||||
|     readonly command: Command; | ||||
|     readonly subcommandInfo: Collection<string, Command>; | ||||
|     readonly keyedSubcommandInfo: Collection<string, NamedCommand>; | ||||
|     readonly permission: number; | ||||
|     readonly nsfw: boolean; | ||||
|     readonly channelType: CHANNEL_TYPE; | ||||
|     readonly args: string[]; | ||||
| } | ||||
| 
 | ||||
| interface CommandInfoError { | ||||
|     readonly type: "error"; | ||||
|     readonly message: string; | ||||
| } | ||||
| 
 | ||||
| interface CommandInfoMetadata { | ||||
|     permission: number; | ||||
|     nsfw: boolean; | ||||
|     channelType: CHANNEL_TYPE; | ||||
|     args: string[]; | ||||
|     usage: string; | ||||
| } | ||||
| 
 | ||||
| export const defaultMetadata = { | ||||
|     permission: 0, | ||||
|     nsfw: false, | ||||
|     channelType: CHANNEL_TYPE.ANY | ||||
| }; | ||||
| 
 | ||||
| export class Command { | ||||
|     public readonly description: string; | ||||
|     public readonly endpoint: boolean; | ||||
|  | @ -90,7 +120,7 @@ export class Command { | |||
|     public readonly nsfw: boolean | null; // null (default) indicates to inherit
 | ||||
|     public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit
 | ||||
|     protected run: (($: CommandMenu) => Promise<any>) | string; | ||||
|     protected readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
 | ||||
|     protected readonly subcommands: Collection<string, NamedCommand>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
 | ||||
|     protected user: Command | null; | ||||
|     protected number: Command | null; | ||||
|     protected any: Command | null; | ||||
|  | @ -124,7 +154,7 @@ export class Command { | |||
|                 // This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object.
 | ||||
|                 for (const name in options.subcommands) { | ||||
|                     const subcmd = options.subcommands[name]; | ||||
|                     subcmd.originalCommandName = name; | ||||
|                     subcmd.name = name; | ||||
|                     const aliases = subcmd.aliases; | ||||
| 
 | ||||
|                     for (const alias of aliases) { | ||||
|  | @ -153,7 +183,7 @@ export class Command { | |||
|         const param = args.shift(); | ||||
| 
 | ||||
|         // If there are no arguments left, execute the current command. Otherwise, continue on.
 | ||||
|         if (!param) { | ||||
|         if (param === undefined) { | ||||
|             // See if there is anything that'll prevent the user from executing the command.
 | ||||
| 
 | ||||
|             // 1. Does this command specify a required channel type? If so, does the channel type match?
 | ||||
|  | @ -218,13 +248,9 @@ export class Command { | |||
|         // If the current command is an endpoint but there are still some arguments left, don't continue.
 | ||||
|         if (this.endpoint) return {content: "Too many arguments!"}; | ||||
| 
 | ||||
|         // If the current command's permission level isn't -1 (inherit), then set the permission metadata equal to that.
 | ||||
|         // Update inherited properties if the current command specifies a property.
 | ||||
|         if (this.permission !== -1) metadata.permission = this.permission; | ||||
| 
 | ||||
|         // If the current command has an NSFW setting specified, set it.
 | ||||
|         if (this.nsfw !== null) metadata.nsfw = this.nsfw; | ||||
| 
 | ||||
|         // If the current command doesn't inherit its channel type, set it.
 | ||||
|         if (this.channelType !== null) metadata.channelType = this.channelType; | ||||
| 
 | ||||
|         // Resolve the value of the current command's argument (adding it to the resolved args),
 | ||||
|  | @ -257,11 +283,97 @@ export class Command { | |||
|         // Note: Do NOT add a return statement here. In case one of the other sections is missing
 | ||||
|         // a return statement, there'll be a compile error to catch that.
 | ||||
|     } | ||||
| 
 | ||||
|     // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
 | ||||
|     public async resolveInfo(args: string[]): Promise<CommandInfo | CommandInfoError> { | ||||
|         return this.resolveInfoInternal(args, {...defaultMetadata, args: [], usage: ""}); | ||||
|     } | ||||
| 
 | ||||
|     private async resolveInfoInternal( | ||||
|         args: string[], | ||||
|         metadata: CommandInfoMetadata | ||||
|     ): Promise<CommandInfo | CommandInfoError> { | ||||
|         const param = args.shift(); | ||||
| 
 | ||||
|         // If there are no arguments left, return the data or an error message.
 | ||||
|         if (param === undefined) { | ||||
|             const keyedSubcommandInfo = new Collection<string, NamedCommand>(); | ||||
|             const subcommandInfo = new Collection<string, Command>(); | ||||
| 
 | ||||
|             // Get all the subcommands of the current command but without aliases.
 | ||||
|             for (const [tag, command] of this.subcommands.entries()) { | ||||
|                 // Don't capture duplicates generated from aliases.
 | ||||
|                 if (tag === command.name) { | ||||
|                     keyedSubcommandInfo.set(tag, command); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Then get all the generic subcommands.
 | ||||
|             if (this.user) subcommandInfo.set("<user>", this.user); | ||||
|             if (this.number) subcommandInfo.set("<number>", this.number); | ||||
|             if (this.any) subcommandInfo.set("<any>", this.any); | ||||
| 
 | ||||
|             return { | ||||
|                 type: "info", | ||||
|                 command: this, | ||||
|                 keyedSubcommandInfo, | ||||
|                 subcommandInfo, | ||||
|                 ...metadata | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         // Update inherited properties if the current command specifies a property.
 | ||||
|         if (this.permission !== -1) metadata.permission = this.permission; | ||||
|         if (this.nsfw !== null) metadata.nsfw = this.nsfw; | ||||
|         if (this.channelType !== null) metadata.channelType = this.channelType; | ||||
|         if (this.usage !== "") metadata.usage = this.usage; | ||||
| 
 | ||||
|         // Then test if anything fits any hardcoded values, otherwise check if it's a valid keyed subcommand.
 | ||||
|         if (param === "<user>") { | ||||
|             if (this.user) { | ||||
|                 metadata.args.push("<user>"); | ||||
|                 return this.user.resolveInfoInternal(args, metadata); | ||||
|             } else { | ||||
|                 return { | ||||
|                     type: "error", | ||||
|                     message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\`` | ||||
|                 }; | ||||
|             } | ||||
|         } else if (param === "<number>") { | ||||
|             if (this.number) { | ||||
|                 metadata.args.push("<number>"); | ||||
|                 return this.number.resolveInfoInternal(args, metadata); | ||||
|             } else { | ||||
|                 return { | ||||
|                     type: "error", | ||||
|                     message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\`` | ||||
|                 }; | ||||
|             } | ||||
|         } else if (param === "<any>") { | ||||
|             if (this.any) { | ||||
|                 metadata.args.push("<any>"); | ||||
|                 return this.any.resolveInfoInternal(args, metadata); | ||||
|             } else { | ||||
|                 return { | ||||
|                     type: "error", | ||||
|                     message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\`` | ||||
|                 }; | ||||
|             } | ||||
|         } else if (this.subcommands?.has(param)) { | ||||
|             metadata.args.push(param); | ||||
|             return this.subcommands.get(param)!.resolveInfoInternal(args, metadata); | ||||
|         } else { | ||||
|             return { | ||||
|                 type: "error", | ||||
|                 message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\`` | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class NamedCommand extends Command { | ||||
|     public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
 | ||||
|     public originalCommandName: string | null; // If the command is an alias, what's the original name?
 | ||||
|     private originalCommandName: string | null; // If the command is an alias, what's the original name?
 | ||||
| 
 | ||||
|     constructor(options?: NamedCommandOptions) { | ||||
|         super(options); | ||||
|  | @ -269,74 +381,14 @@ export class NamedCommand extends Command { | |||
|         this.originalCommandName = null; | ||||
|     } | ||||
| 
 | ||||
|     // Returns: [category, command name, command, available subcommands: [type, subcommand]]
 | ||||
|     public async resolveInfo(args: string[]): [string, string, Command, Collection<string, Command>] | null { | ||||
|         // For debug info, use this.originalCommandName? (if it exists?)
 | ||||
|         const commands = await loadableCommands; | ||||
|         let header = args.shift(); | ||||
|         let command = commands.get(header); | ||||
| 
 | ||||
|         if (!command || header === "test") { | ||||
|             $.channel.send(`No command found by the name \`${header}\`!`); | ||||
|             return; | ||||
|     public get name(): string { | ||||
|         if (this.originalCommandName === null) throw new Error("originalCommandName must be set before accessing it!"); | ||||
|         else return this.originalCommandName; | ||||
|     } | ||||
| 
 | ||||
|         if (command.originalCommandName) header = command.originalCommandName; | ||||
|         else console.warn(`originalCommandName isn't defined for ${header}?!`); | ||||
| 
 | ||||
|         let permLevel = command.permission ?? 0; | ||||
|         let usage = command.usage; | ||||
|         let invalid = false; | ||||
| 
 | ||||
|         let selectedCategory = "Unknown"; | ||||
| 
 | ||||
|         for (const [category, headers] of categories) { | ||||
|             if (headers.includes(header)) { | ||||
|                 if (selectedCategory !== "Unknown") | ||||
|                     console.warn( | ||||
|                         `Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.` | ||||
|                     ); | ||||
|                 else selectedCategory = toTitleCase(category); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (const param of args) { | ||||
|             const type = command.resolve(param); | ||||
|             command = command.get(param); | ||||
|             permLevel = command.permission ?? permLevel; | ||||
| 
 | ||||
|             if (permLevel === -1) permLevel = command.permission; | ||||
| 
 | ||||
|             // Switch over to doing `$help info <user>`
 | ||||
|             switch (type) { | ||||
|                 case TYPES.SUBCOMMAND: | ||||
|                     header += ` ${command.originalCommandName}`; | ||||
|                     break; | ||||
|                 case TYPES.USER: | ||||
|                     header += " <user>"; | ||||
|                     break; | ||||
|                 case TYPES.NUMBER: | ||||
|                     header += " <number>"; | ||||
|                     break; | ||||
|                 case TYPES.ANY: | ||||
|                     header += " <any>"; | ||||
|                     break; | ||||
|                 case TYPES.NONE: | ||||
|                     header += ` ${param}`; | ||||
|                     break; | ||||
|                 default: | ||||
|                     requireAllCasesHandledFor(type); | ||||
|             } | ||||
| 
 | ||||
|             if (type === TYPES.NONE) { | ||||
|                 invalid = true; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (invalid) { | ||||
|             $.channel.send(`No command found by the name \`${header}\`!`); | ||||
|             return; | ||||
|         } | ||||
|     public set name(value: string) { | ||||
|         if (this.originalCommandName !== null) | ||||
|             throw new Error(`originalCommandName cannot be set twice! Attempted to set the value to "${value}".`); | ||||
|         else this.originalCommandName = value; | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import {loadableCommands} from "./loader"; | |||
| import {Permissions, Message} from "discord.js"; | ||||
| import {getPrefix} from "./structures"; | ||||
| import {Config} from "./structures"; | ||||
| import {CHANNEL_TYPE} from "./command"; | ||||
| import {defaultMetadata} from "./command"; | ||||
| 
 | ||||
| // For custom message events that want to cancel the command handler on certain conditions.
 | ||||
| const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot]; | ||||
|  | @ -12,12 +12,6 @@ export function addInterceptRule(handler: (message: Message) => boolean) { | |||
|     interceptRules.push(handler); | ||||
| } | ||||
| 
 | ||||
| const defaultMetadata = { | ||||
|     permission: 0, | ||||
|     nsfw: false, | ||||
|     channelType: CHANNEL_TYPE.ANY | ||||
| }; | ||||
| 
 | ||||
| // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined.
 | ||||
| // Note: guild.available will never need to be checked because the message starts in either a DM channel or an already-available guild.
 | ||||
| client.on("message", async (message) => { | ||||
|  | @ -51,7 +45,7 @@ client.on("message", async (message) => { | |||
|             // Send the arguments to the command to resolve and execute.
 | ||||
|             const result = await command.execute(args, menu, { | ||||
|                 header, | ||||
|                 args, | ||||
|                 args: [...args], | ||||
|                 ...defaultMetadata | ||||
|             }); | ||||
| 
 | ||||
|  | @ -83,7 +77,7 @@ client.on("message", async (message) => { | |||
|                 // Send the arguments to the command to resolve and execute.
 | ||||
|                 const result = await command.execute(args, menu, { | ||||
|                     header, | ||||
|                     args, | ||||
|                     args: [...args], | ||||
|                     ...defaultMetadata | ||||
|                 }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ export const loadableCommands = (async () => { | |||
|             const command = (await import(`../commands/${commandID}`)).default as unknown; | ||||
| 
 | ||||
|             if (command instanceof NamedCommand) { | ||||
|                 command.originalCommandName = commandName; | ||||
|                 command.name = commandName; | ||||
| 
 | ||||
|                 if (commands.has(commandName)) { | ||||
|                     console.warn( | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue