mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Added rest subcommand type
This commit is contained in:
		
							parent
							
								
									e1e6910b1d
								
							
						
					
					
						commit
						26e0bb5824
					
				
					 6 changed files with 245 additions and 122 deletions
				
			
		|  | @ -9,8 +9,9 @@ | ||||||
| - `urban`: Bug fixes | - `urban`: Bug fixes | ||||||
| - Changed `help` to display a paginated embed | - Changed `help` to display a paginated embed | ||||||
| - Various changes to core | - Various changes to core | ||||||
| 	- Added `guild` subcommand type (only accessible when `id` is set to `guild`) | 	- Added `guild` subcommand type (only accessible when `id: "guild"`) | ||||||
| 	- Further reduced `channel.send()` to `send()` because it's used in *every, single, command* | 	- Further reduced `channel.send()` to `send()` because it's used in *every, single, command* | ||||||
|  | 	- Added `rest` subcommand type (only available when `endpoint: true`), declaratively states that the following command will do `args.join(" ")`, preventing any other subcommands from being added | ||||||
| 
 | 
 | ||||||
| # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) | # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) | ||||||
| - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. | - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. | ||||||
|  |  | ||||||
|  | @ -33,8 +33,9 @@ export default new NamedCommand({ | ||||||
|     }, |     }, | ||||||
|     any: new Command({ |     any: new Command({ | ||||||
|         async run({send, message, channel, guild, author, member, client, args}) { |         async run({send, message, channel, guild, author, member, client, args}) { | ||||||
|             const [result, category] = await getCommandInfo(args); |             const resultingBlob = await getCommandInfo(args); | ||||||
|             if (typeof result === "string") return send(result); |             if (typeof resultingBlob === "string") return send(resultingBlob); | ||||||
|  |             const [result, category] = resultingBlob; | ||||||
|             let append = ""; |             let append = ""; | ||||||
|             const command = result.command; |             const command = result.command; | ||||||
|             const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; |             const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; | ||||||
|  | @ -52,6 +53,7 @@ export default new NamedCommand({ | ||||||
|                     list.push(`❯ \`${header} ${type}${customUsage}\` - ${subcommand.description}`); |                     list.push(`❯ \`${header} ${type}${customUsage}\` - ${subcommand.description}`); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  |                 if (result.hasRestCommand) list.push(`❯ \`${header} <...>\``); | ||||||
|                 append = list.length > 0 ? list.join("\n") : "None"; |                 append = list.length > 0 ? list.join("\n") : "None"; | ||||||
|             } else { |             } else { | ||||||
|                 append = `\`${header} ${command.usage}\``; |                 append = `\`${header} ${command.usage}\``; | ||||||
|  |  | ||||||
|  | @ -280,7 +280,7 @@ export default new NamedCommand({ | ||||||
|                             } |                             } | ||||||
|                         } else { |                         } else { | ||||||
|                             // If it's a unique hour, just search through the tuple list and find the matching entry.
 |                             // If it's a unique hour, just search through the tuple list and find the matching entry.
 | ||||||
|                             for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { |                             for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { | ||||||
|                                 if (hour === hourPoint) { |                                 if (hour === hourPoint) { | ||||||
|                                     profile.timezone = timezoneOffset; |                                     profile.timezone = timezoneOffset; | ||||||
|                                 } |                                 } | ||||||
|  |  | ||||||
|  | @ -78,11 +78,12 @@ interface CommandOptionsBase { | ||||||
|     readonly permission?: number; |     readonly permission?: number; | ||||||
|     readonly nsfw?: boolean; |     readonly nsfw?: boolean; | ||||||
|     readonly channelType?: CHANNEL_TYPE; |     readonly channelType?: CHANNEL_TYPE; | ||||||
|     readonly run?: (($: CommandMenu) => Promise<any>) | string; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface CommandOptionsEndpoint { | interface CommandOptionsEndpoint { | ||||||
|     readonly endpoint: true; |     readonly endpoint: true; | ||||||
|  |     readonly rest?: RestCommand; | ||||||
|  |     readonly run?: (($: CommandMenu) => Promise<any>) | string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Prevents subcommands from being added by compile-time.
 | // Prevents subcommands from being added by compile-time.
 | ||||||
|  | @ -100,10 +101,15 @@ interface CommandOptionsNonEndpoint { | ||||||
|     readonly id?: ID; |     readonly id?: ID; | ||||||
|     readonly number?: Command; |     readonly number?: Command; | ||||||
|     readonly any?: Command; |     readonly any?: Command; | ||||||
|  |     readonly rest?: undefined; // Redeclare it here as undefined to prevent its use otherwise.
 | ||||||
|  |     readonly run?: (($: CommandMenu) => Promise<any>) | string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type CommandOptions = CommandOptionsBase & (CommandOptionsEndpoint | CommandOptionsNonEndpoint); | type CommandOptions = CommandOptionsBase & (CommandOptionsEndpoint | CommandOptionsNonEndpoint); | ||||||
| type NamedCommandOptions = CommandOptions & {aliases?: string[]}; | type NamedCommandOptions = CommandOptions & {aliases?: string[]; nameOverride?: string}; | ||||||
|  | type RestCommandOptions = CommandOptionsBase & { | ||||||
|  |     run?: (($: CommandMenu & {readonly combined: string}) => Promise<any>) | string; | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| interface ExecuteCommandMetadata { | interface ExecuteCommandMetadata { | ||||||
|     readonly header: string; |     readonly header: string; | ||||||
|  | @ -116,9 +122,10 @@ interface ExecuteCommandMetadata { | ||||||
| 
 | 
 | ||||||
| export interface CommandInfo { | export interface CommandInfo { | ||||||
|     readonly type: "info"; |     readonly type: "info"; | ||||||
|     readonly command: Command; |     readonly command: BaseCommand; | ||||||
|     readonly subcommandInfo: Collection<string, Command>; |     readonly subcommandInfo: Collection<string, Command>; | ||||||
|     readonly keyedSubcommandInfo: Collection<string, NamedCommand>; |     readonly keyedSubcommandInfo: Collection<string, NamedCommand>; | ||||||
|  |     readonly hasRestCommand: boolean; | ||||||
|     readonly permission: number; |     readonly permission: number; | ||||||
|     readonly nsfw: boolean; |     readonly nsfw: boolean; | ||||||
|     readonly channelType: CHANNEL_TYPE; |     readonly channelType: CHANNEL_TYPE; | ||||||
|  | @ -141,14 +148,26 @@ interface CommandInfoMetadata { | ||||||
|     readonly header: string; |     readonly header: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Each Command instance represents a block that links other Command instances under it.
 | // An isolated command of just the metadata.
 | ||||||
| export class Command { | abstract class BaseCommand { | ||||||
|     public readonly description: string; |     public readonly description: string; | ||||||
|     public readonly endpoint: boolean; |  | ||||||
|     public readonly usage: string; |     public readonly usage: string; | ||||||
|     public readonly permission: number; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on.
 |     public readonly permission: number; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on.
 | ||||||
|     public readonly nsfw: boolean | null; // null (default) indicates to inherit
 |     public readonly nsfw: boolean | null; // null (default) indicates to inherit
 | ||||||
|     public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit
 |     public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit
 | ||||||
|  | 
 | ||||||
|  |     constructor(options?: CommandOptionsBase) { | ||||||
|  |         this.description = options?.description || "No description."; | ||||||
|  |         this.usage = options?.usage ?? ""; | ||||||
|  |         this.permission = options?.permission ?? -1; | ||||||
|  |         this.nsfw = options?.nsfw ?? null; | ||||||
|  |         this.channelType = options?.channelType ?? null; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Each Command instance represents a block that links other Command instances under it.
 | ||||||
|  | export class Command extends BaseCommand { | ||||||
|  |     public readonly endpoint: boolean; | ||||||
|     // The execute and subcommand properties are restricted to the class because subcommand recursion could easily break when manually handled.
 |     // The execute and subcommand properties are restricted to the class because subcommand recursion could easily break when manually handled.
 | ||||||
|     // The class will handle checking for null fields.
 |     // The class will handle checking for null fields.
 | ||||||
|     private run: (($: CommandMenu) => Promise<any>) | string; |     private run: (($: CommandMenu) => Promise<any>) | string; | ||||||
|  | @ -163,14 +182,11 @@ export class Command { | ||||||
|     private idType: ID | null; |     private idType: ID | null; | ||||||
|     private number: Command | null; |     private number: Command | null; | ||||||
|     private any: Command | null; |     private any: Command | null; | ||||||
|  |     private rest: RestCommand | null; | ||||||
| 
 | 
 | ||||||
|     constructor(options?: CommandOptions) { |     constructor(options?: CommandOptions) { | ||||||
|         this.description = options?.description || "No description."; |         super(options); | ||||||
|         this.endpoint = !!options?.endpoint; |         this.endpoint = !!options?.endpoint; | ||||||
|         this.usage = options?.usage ?? ""; |  | ||||||
|         this.permission = options?.permission ?? -1; |  | ||||||
|         this.nsfw = options?.nsfw ?? null; |  | ||||||
|         this.channelType = options?.channelType ?? null; |  | ||||||
|         this.run = options?.run || "No action was set on this command!"; |         this.run = options?.run || "No action was set on this command!"; | ||||||
|         this.subcommands = new Collection(); // Populate this collection after setting subcommands.
 |         this.subcommands = new Collection(); // Populate this collection after setting subcommands.
 | ||||||
|         this.channel = null; |         this.channel = null; | ||||||
|  | @ -183,19 +199,19 @@ export class Command { | ||||||
|         this.idType = null; |         this.idType = null; | ||||||
|         this.number = null; |         this.number = null; | ||||||
|         this.any = null; |         this.any = null; | ||||||
|  |         this.rest = null; | ||||||
| 
 | 
 | ||||||
|         if (options && !options.endpoint) { |         if (options && !options.endpoint) { | ||||||
|             if (options?.channel) this.channel = options.channel; |             if (options.channel) this.channel = options.channel; | ||||||
|             if (options?.role) this.role = options.role; |             if (options.role) this.role = options.role; | ||||||
|             if (options?.emote) this.emote = options.emote; |             if (options.emote) this.emote = options.emote; | ||||||
|             if (options?.message) this.message = options.message; |             if (options.message) this.message = options.message; | ||||||
|             if (options?.user) this.user = options.user; |             if (options.user) this.user = options.user; | ||||||
|             if (options?.guild) this.guild = options.guild; |             if (options.guild) this.guild = options.guild; | ||||||
|             if (options?.number) this.number = options.number; |             if (options.number) this.number = options.number; | ||||||
|             if (options?.any) this.any = options.any; |             if (options.any) this.any = options.any; | ||||||
|             if (options?.id) this.idType = options.id; |             if (options.id) this.idType = options.id; | ||||||
| 
 | 
 | ||||||
|             if (options?.id) { |  | ||||||
|             switch (options.id) { |             switch (options.id) { | ||||||
|                 case "channel": |                 case "channel": | ||||||
|                     this.id = this.channel; |                     this.id = this.channel; | ||||||
|  | @ -215,12 +231,13 @@ export class Command { | ||||||
|                 case "guild": |                 case "guild": | ||||||
|                     this.id = this.guild; |                     this.id = this.guild; | ||||||
|                     break; |                     break; | ||||||
|  |                 case undefined: | ||||||
|  |                     break; | ||||||
|                 default: |                 default: | ||||||
|                     requireAllCasesHandledFor(options.id); |                     requireAllCasesHandledFor(options.id); | ||||||
|             } |             } | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if (options?.subcommands) { |             if (options.subcommands) { | ||||||
|                 const baseSubcommands = Object.keys(options.subcommands); |                 const baseSubcommands = Object.keys(options.subcommands); | ||||||
| 
 | 
 | ||||||
|                 // Loop once to set the base subcommands.
 |                 // Loop once to set the base subcommands.
 | ||||||
|  | @ -246,6 +263,8 @@ export class Command { | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |         } else if (options && options.endpoint) { | ||||||
|  |             if (options.rest) this.rest = options.rest; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -273,41 +292,9 @@ export class Command { | ||||||
| 
 | 
 | ||||||
|         // If there are no arguments left, execute the current command. Otherwise, continue on.
 |         // If there are no arguments left, execute the current command. Otherwise, continue on.
 | ||||||
|         if (param === undefined) { |         if (param === undefined) { | ||||||
|             // See if there is anything that'll prevent the user from executing the command.
 |             const error = canExecute(menu, metadata); | ||||||
|  |             if (error) return error; | ||||||
| 
 | 
 | ||||||
|             // 1. Does this command specify a required channel type? If so, does the channel type match?
 |  | ||||||
|             if ( |  | ||||||
|                 metadata.channelType === CHANNEL_TYPE.GUILD && |  | ||||||
|                 (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) |  | ||||||
|             ) { |  | ||||||
|                 return {content: "This command must be executed in a server."}; |  | ||||||
|             } else if ( |  | ||||||
|                 metadata.channelType === CHANNEL_TYPE.DM && |  | ||||||
|                 (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) |  | ||||||
|             ) { |  | ||||||
|                 return {content: "This command must be executed as a direct message."}; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.)
 |  | ||||||
|             if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { |  | ||||||
|                 return {content: "This command must be executed in either an NSFW channel or as a direct message."}; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // 3. Does the user have permission to execute the command?
 |  | ||||||
|             if (!hasPermission(menu.author, menu.member, metadata.permission)) { |  | ||||||
|                 const userPermLevel = getPermissionLevel(menu.author, menu.member); |  | ||||||
| 
 |  | ||||||
|                 return { |  | ||||||
|                     content: `You don't have access to this command! Your permission level is \`${getPermissionName( |  | ||||||
|                         userPermLevel |  | ||||||
|                     )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( |  | ||||||
|                         metadata.permission |  | ||||||
|                     )}\` (${metadata.permission}).` |  | ||||||
|                 }; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // Then capture any potential errors.
 |  | ||||||
|             try { |  | ||||||
|             if (typeof this.run === "string") { |             if (typeof this.run === "string") { | ||||||
|                 // Although I *could* add an option in the launcher to attach arbitrary variables to this var string...
 |                 // Although I *could* add an option in the launcher to attach arbitrary variables to this var string...
 | ||||||
|                 // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string.
 |                 // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string.
 | ||||||
|  | @ -323,10 +310,9 @@ export class Command { | ||||||
|                     ) |                     ) | ||||||
|                 ); |                 ); | ||||||
|             } else { |             } else { | ||||||
|  |                 // Then capture any potential errors.
 | ||||||
|  |                 try { | ||||||
|                     await this.run(menu); |                     await this.run(menu); | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 return null; |  | ||||||
|                 } catch (error) { |                 } catch (error) { | ||||||
|                     const errorMessage = error.stack ?? error; |                     const errorMessage = error.stack ?? error; | ||||||
|                     console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); |                     console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); | ||||||
|  | @ -337,8 +323,18 @@ export class Command { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|         // If the current command is an endpoint but there are still some arguments left, don't continue.
 |             return null; | ||||||
|         if (this.endpoint) return {content: "Too many arguments!"}; |         } | ||||||
|  | 
 | ||||||
|  |         // If the current command is an endpoint but there are still some arguments left, don't continue unless there's a RestCommand.
 | ||||||
|  |         if (this.endpoint) { | ||||||
|  |             if (this.rest) { | ||||||
|  |                 args.unshift(param); | ||||||
|  |                 return this.rest.execute(args.join(" "), menu, metadata); | ||||||
|  |             } else { | ||||||
|  |                 return {content: "Too many arguments!"}; | ||||||
|  |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         // Resolve the value of the current command's argument (adding it to the resolved args),
 |         // Resolve the value of the current command's argument (adding it to the resolved args),
 | ||||||
|         // then pass the thread of execution to whichever subcommand is valid (if any).
 |         // then pass the thread of execution to whichever subcommand is valid (if any).
 | ||||||
|  | @ -532,7 +528,7 @@ export class Command { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
 |     // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
 | ||||||
|     public async resolveInfo(args: string[], header: string): Promise<CommandInfo | CommandInfoError> { |     public resolveInfo(args: string[], header: string): CommandInfo | CommandInfoError { | ||||||
|         return this.resolveInfoInternal(args, { |         return this.resolveInfoInternal(args, { | ||||||
|             permission: 0, |             permission: 0, | ||||||
|             nsfw: false, |             nsfw: false, | ||||||
|  | @ -544,10 +540,7 @@ export class Command { | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async resolveInfoInternal( |     private resolveInfoInternal(args: string[], metadata: CommandInfoMetadata): CommandInfo | CommandInfoError { | ||||||
|         args: string[], |  | ||||||
|         metadata: CommandInfoMetadata |  | ||||||
|     ): Promise<CommandInfo | CommandInfoError> { |  | ||||||
|         // Update inherited properties if the current command specifies a property.
 |         // Update inherited properties if the current command specifies a property.
 | ||||||
|         // In case there are no initial arguments, these should go first so that it can register.
 |         // In case there are no initial arguments, these should go first so that it can register.
 | ||||||
|         if (this.permission !== -1) metadata.permission = this.permission; |         if (this.permission !== -1) metadata.permission = this.permission; | ||||||
|  | @ -586,6 +579,7 @@ export class Command { | ||||||
|                 command: this, |                 command: this, | ||||||
|                 keyedSubcommandInfo, |                 keyedSubcommandInfo, | ||||||
|                 subcommandInfo, |                 subcommandInfo, | ||||||
|  |                 hasRestCommand: !!this.rest, | ||||||
|                 ...metadata |                 ...metadata | ||||||
|             }; |             }; | ||||||
|         } |         } | ||||||
|  | @ -652,6 +646,13 @@ export class Command { | ||||||
|             } else { |             } else { | ||||||
|                 return invalidSubcommandGenerator(); |                 return invalidSubcommandGenerator(); | ||||||
|             } |             } | ||||||
|  |         } else if (param === "<...>") { | ||||||
|  |             if (this.rest) { | ||||||
|  |                 metadata.args.push("<...>"); | ||||||
|  |                 return this.rest.resolveInfoFinale(metadata); | ||||||
|  |             } else { | ||||||
|  |                 return invalidSubcommandGenerator(); | ||||||
|  |             } | ||||||
|         } else if (this.subcommands?.has(param)) { |         } else if (this.subcommands?.has(param)) { | ||||||
|             metadata.args.push(param); |             metadata.args.push(param); | ||||||
|             return this.subcommands.get(param)!.resolveInfoInternal(args, metadata); |             return this.subcommands.get(param)!.resolveInfoInternal(args, metadata); | ||||||
|  | @ -668,7 +669,8 @@ export class NamedCommand extends Command { | ||||||
|     constructor(options?: NamedCommandOptions) { |     constructor(options?: NamedCommandOptions) { | ||||||
|         super(options); |         super(options); | ||||||
|         this.aliases = options?.aliases || []; |         this.aliases = options?.aliases || []; | ||||||
|         this.originalCommandName = null; |         // The name override exists in case a user wants to bypass filename restrictions.
 | ||||||
|  |         this.originalCommandName = options?.nameOverride ?? null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public get name(): string { |     public get name(): string { | ||||||
|  | @ -681,4 +683,119 @@ export class NamedCommand extends Command { | ||||||
|             throw new Error(`originalCommandName cannot be set twice! Attempted to set the value to "${value}".`); |             throw new Error(`originalCommandName cannot be set twice! Attempted to set the value to "${value}".`); | ||||||
|         else this.originalCommandName = value; |         else this.originalCommandName = value; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public isNameSet(): boolean { | ||||||
|  |         return this.originalCommandName !== null; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RestCommand is a declarative version of the common "any: args.join(' ')" pattern, basically the Command version of a rest parameter.
 | ||||||
|  | // This way, you avoid having extra subcommands when using this pattern.
 | ||||||
|  | // I'm probably not going to add a transformer function (a callback to automatically handle stuff like searching for usernames).
 | ||||||
|  | // I don't think the effort to figure this part out via generics or something is worth it.
 | ||||||
|  | export class RestCommand extends BaseCommand { | ||||||
|  |     private run: (($: CommandMenu & {readonly combined: string}) => Promise<any>) | string; | ||||||
|  | 
 | ||||||
|  |     constructor(options?: RestCommandOptions) { | ||||||
|  |         super(options); | ||||||
|  |         this.run = options?.run || "No action was set on this command!"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async execute( | ||||||
|  |         combined: string, | ||||||
|  |         menu: CommandMenu, | ||||||
|  |         metadata: ExecuteCommandMetadata | ||||||
|  |     ): Promise<SingleMessageOptions | null> { | ||||||
|  |         // Update inherited properties if the current command specifies a property.
 | ||||||
|  |         // In case there are no initial arguments, these should go first so that it can register.
 | ||||||
|  |         if (this.permission !== -1) metadata.permission = this.permission; | ||||||
|  |         if (this.nsfw !== null) metadata.nsfw = this.nsfw; | ||||||
|  |         if (this.channelType !== null) metadata.channelType = this.channelType; | ||||||
|  | 
 | ||||||
|  |         const error = canExecute(menu, metadata); | ||||||
|  |         if (error) return error; | ||||||
|  | 
 | ||||||
|  |         if (typeof this.run === "string") { | ||||||
|  |             // Although I *could* add an option in the launcher to attach arbitrary variables to this var string...
 | ||||||
|  |             // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string.
 | ||||||
|  |             await menu.send( | ||||||
|  |                 parseVars( | ||||||
|  |                     this.run, | ||||||
|  |                     { | ||||||
|  |                         author: menu.author.toString(), | ||||||
|  |                         prefix: getPrefix(menu.guild), | ||||||
|  |                         command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` | ||||||
|  |                     }, | ||||||
|  |                     "???" | ||||||
|  |                 ) | ||||||
|  |             ); | ||||||
|  |         } else { | ||||||
|  |             // Then capture any potential errors.
 | ||||||
|  |             try { | ||||||
|  |                 await this.run({...menu, combined}); | ||||||
|  |             } catch (error) { | ||||||
|  |                 const errorMessage = error.stack ?? error; | ||||||
|  |                 console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); | ||||||
|  | 
 | ||||||
|  |                 return { | ||||||
|  |                     content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public resolveInfoFinale(metadata: CommandInfoMetadata): CommandInfo { | ||||||
|  |         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; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             type: "info", | ||||||
|  |             command: this, | ||||||
|  |             keyedSubcommandInfo: new Collection<string, NamedCommand>(), | ||||||
|  |             subcommandInfo: new Collection<string, Command>(), | ||||||
|  |             hasRestCommand: false, | ||||||
|  |             ...metadata | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // See if there is anything that'll prevent the user from executing the command.
 | ||||||
|  | // Returns null if successful, otherwise returns a message with the error.
 | ||||||
|  | function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): SingleMessageOptions | null { | ||||||
|  |     // 1. Does this command specify a required channel type? If so, does the channel type match?
 | ||||||
|  |     if ( | ||||||
|  |         metadata.channelType === CHANNEL_TYPE.GUILD && | ||||||
|  |         (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) | ||||||
|  |     ) { | ||||||
|  |         return {content: "This command must be executed in a server."}; | ||||||
|  |     } else if ( | ||||||
|  |         metadata.channelType === CHANNEL_TYPE.DM && | ||||||
|  |         (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) | ||||||
|  |     ) { | ||||||
|  |         return {content: "This command must be executed as a direct message."}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.)
 | ||||||
|  |     if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { | ||||||
|  |         return {content: "This command must be executed in either an NSFW channel or as a direct message."}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // 3. Does the user have permission to execute the command?
 | ||||||
|  |     if (!hasPermission(menu.author, menu.member, metadata.permission)) { | ||||||
|  |         const userPermLevel = getPermissionLevel(menu.author, menu.member); | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             content: `You don't have access to this command! Your permission level is \`${getPermissionName( | ||||||
|  |                 userPermLevel | ||||||
|  |             )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( | ||||||
|  |                 metadata.permission | ||||||
|  |             )}\` (${metadata.permission}).` | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| // Onion Lasers Command Handler //
 | // Onion Lasers Command Handler //
 | ||||||
| export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; | export {Command, NamedCommand, RestCommand, CHANNEL_TYPE} from "./command"; | ||||||
| export {addInterceptRule} from "./handler"; | export {addInterceptRule} from "./handler"; | ||||||
| export {launch} from "./interface"; | export {launch} from "./interface"; | ||||||
| export * from "./libd"; | export * from "./libd"; | ||||||
|  |  | ||||||
|  | @ -43,14 +43,16 @@ export async function loadCommands(commandsDir: string): Promise<Collection<stri | ||||||
|             const command = (await import(files[i])).default as unknown; |             const command = (await import(files[i])).default as unknown; | ||||||
| 
 | 
 | ||||||
|             if (command instanceof NamedCommand) { |             if (command instanceof NamedCommand) { | ||||||
|                 command.name = commandName; |                 const isNameOverridden = command.isNameSet(); | ||||||
|  |                 if (!isNameOverridden) command.name = commandName; | ||||||
|  |                 const header = command.name; | ||||||
| 
 | 
 | ||||||
|                 if (commands.has(commandName)) { |                 if (commands.has(header)) { | ||||||
|                     console.warn( |                     console.warn( | ||||||
|                         `Command "${commandName}" already exists! Make sure to make each command uniquely identifiable across categories!` |                         `Command "${header}" already exists! Make sure to make each command uniquely identifiable across categories!` | ||||||
|                     ); |                     ); | ||||||
|                 } else { |                 } else { | ||||||
|                     commands.set(commandName, command); |                     commands.set(header, command); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 for (const alias of command.aliases) { |                 for (const alias of command.aliases) { | ||||||
|  | @ -64,9 +66,10 @@ export async function loadCommands(commandsDir: string): Promise<Collection<stri | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (!(category in lists)) lists[category] = []; |                 if (!(category in lists)) lists[category] = []; | ||||||
|                 lists[category].push(commandName); |                 lists[category].push(header); | ||||||
| 
 | 
 | ||||||
|                 console.log(`Loaded Command: ${commandID}`); |                 if (isNameOverridden) console.log(`Loaded Command: "${commandID}" as "${header}"`); | ||||||
|  |                 else console.log(`Loaded Command: ${commandID}`); | ||||||
|             } else { |             } else { | ||||||
|                 console.warn(`Command "${commandID}" has no default export which is a NamedCommand instance!`); |                 console.warn(`Command "${commandID}" has no default export which is a NamedCommand instance!`); | ||||||
|             } |             } | ||||||
|  | @ -139,7 +142,7 @@ export async function getCommandInfo(args: string[]): Promise<[CommandInfo, stri | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Gather info
 |     // Gather info
 | ||||||
|     const result = await command.resolveInfo(args, header); |     const result = command.resolveInfo(args, header); | ||||||
|     if (result.type === "error") return result.message; |     if (result.type === "error") return result.message; | ||||||
|     else return [result, category]; |     else return [result, category]; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue