Merge pull request #136 from MierenManz/main
Argument parsing for commands
This commit is contained in:
		
						commit
						60e7c75113
					
				
					 5 changed files with 264 additions and 5 deletions
				
			
		
							
								
								
									
										1
									
								
								mod.ts
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								mod.ts
									
										
									
									
									
								
							|  | @ -191,3 +191,4 @@ export { | ||||||
|   isVoiceChannel, |   isVoiceChannel, | ||||||
|   default as getChannelByType |   default as getChannelByType | ||||||
| } from './src/utils/channel.ts' | } from './src/utils/channel.ts' | ||||||
|  | export * from "./src/utils/command.ts" | ||||||
|  | @ -9,6 +9,7 @@ import { | ||||||
|   CommandsManager, |   CommandsManager, | ||||||
|   parseCommand |   parseCommand | ||||||
| } from './command.ts' | } from './command.ts' | ||||||
|  | import { parseArgs } from '../utils/command.ts' | ||||||
| import { Extension, ExtensionsManager } from './extension.ts' | import { Extension, ExtensionsManager } from './extension.ts' | ||||||
| 
 | 
 | ||||||
| type PrefixReturnType = string | string[] | Promise<string | string[]> | type PrefixReturnType = string | string[] | Promise<string | string[]> | ||||||
|  | @ -239,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions { | ||||||
|       client: this, |       client: this, | ||||||
|       name: parsed.name, |       name: parsed.name, | ||||||
|       prefix, |       prefix, | ||||||
|       args: parsed.args, |       args: parseArgs(command.args, parsed.args), | ||||||
|       argString: parsed.argString, |       argString: parsed.argString, | ||||||
|       message: msg, |       message: msg, | ||||||
|       author: msg.author, |       author: msg.author, | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import { Collection } from '../utils/collection.ts' | ||||||
| import type { CommandClient } from './client.ts' | import type { CommandClient } from './client.ts' | ||||||
| import type { Extension } from './extension.ts' | import type { Extension } from './extension.ts' | ||||||
| import { join, walk } from '../../deps.ts' | import { join, walk } from '../../deps.ts' | ||||||
| 
 | import type { Args } from '../utils/command.ts' | ||||||
| export interface CommandContext { | export interface CommandContext { | ||||||
|   /** The Client object */ |   /** The Client object */ | ||||||
|   client: CommandClient |   client: CommandClient | ||||||
|  | @ -23,7 +23,7 @@ export interface CommandContext { | ||||||
|   /** Name of Command which was used */ |   /** Name of Command which was used */ | ||||||
|   name: string |   name: string | ||||||
|   /** Array of Arguments used with Command */ |   /** Array of Arguments used with Command */ | ||||||
|   args: string[] |   args: Record<string, unknown> | null | ||||||
|   /** Complete Raw String of Arguments */ |   /** Complete Raw String of Arguments */ | ||||||
|   argString: string |   argString: string | ||||||
|   /** Guild which the command has called */ |   /** Guild which the command has called */ | ||||||
|  | @ -46,7 +46,7 @@ export interface CommandOptions { | ||||||
|   /** Usage Example of Command, only Arguments (without Prefix and Name) */ |   /** Usage Example of Command, only Arguments (without Prefix and Name) */ | ||||||
|   examples?: string | string[] |   examples?: string | string[] | ||||||
|   /** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */ |   /** Does the Command take Arguments? Maybe number of required arguments? Or list of arguments? */ | ||||||
|   args?: number | boolean | string[] |   args?: Args[] | ||||||
|   /** Permissions(s) required by both User and Bot in order to use Command */ |   /** Permissions(s) required by both User and Bot in order to use Command */ | ||||||
|   permissions?: string | string[] |   permissions?: string | string[] | ||||||
|   /** Permission(s) required for using Command */ |   /** Permission(s) required for using Command */ | ||||||
|  | @ -81,7 +81,7 @@ export class Command implements CommandOptions { | ||||||
|   extension?: Extension |   extension?: Extension | ||||||
|   usage?: string | string[] |   usage?: string | string[] | ||||||
|   examples?: string | string[] |   examples?: string | string[] | ||||||
|   args?: number | boolean | string[] |   args?: Args[] | ||||||
|   permissions?: string | string[] |   permissions?: string | string[] | ||||||
|   userPermissions?: string | string[] |   userPermissions?: string | string[] | ||||||
|   botPermissions?: string | string[] |   botPermissions?: string | string[] | ||||||
|  |  | ||||||
							
								
								
									
										107
									
								
								src/utils/command.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/utils/command.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,107 @@ | ||||||
|  | interface MentionToRegex { | ||||||
|  |   [key: string]: RegExp | ||||||
|  |   mentionUser: RegExp | ||||||
|  |   mentionRole: RegExp | ||||||
|  |   mentionChannel: RegExp | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const mentionToRegex: MentionToRegex = { | ||||||
|  |   mentionUser: /<@!?(\d{17,19})>/, | ||||||
|  |   mentionRole: /<@&(\d{17,19})>/, | ||||||
|  |   mentionChannel: /<#(\d{17,19})>/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type CommandArgumentMatchTypes = | ||||||
|  |   | 'flag' | ||||||
|  |   | 'mentionUser' | ||||||
|  |   | 'mentionRole' | ||||||
|  |   | 'mentionChannel' | ||||||
|  |   | 'content' | ||||||
|  |   | 'rest' | ||||||
|  | 
 | ||||||
|  | export interface Args<T = unknown> { | ||||||
|  |   name: string | ||||||
|  |   match: CommandArgumentMatchTypes | ||||||
|  |   defaultValue?: T | ||||||
|  |   flag?: string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function parseArgs( | ||||||
|  |   commandArgs: Args[] | undefined, | ||||||
|  |   messageArgs: string[] | ||||||
|  | ): Record<string, unknown> | null { | ||||||
|  |   if (commandArgs === undefined) return null | ||||||
|  | 
 | ||||||
|  |   const messageArgsNullableCopy: Array<string | null> = [...messageArgs] | ||||||
|  |   const args: Record<string, unknown> = {} | ||||||
|  | 
 | ||||||
|  |   for (const entry of commandArgs) { | ||||||
|  |     switch (entry.match) { | ||||||
|  |       case 'flag': | ||||||
|  |         parseFlags(args, entry, messageArgsNullableCopy) | ||||||
|  |         break | ||||||
|  |       case 'mentionUser': | ||||||
|  |       case 'mentionRole': | ||||||
|  |       case 'mentionChannel': | ||||||
|  |         parseMention(args, entry, messageArgsNullableCopy) | ||||||
|  |         break | ||||||
|  |       case 'content': | ||||||
|  |         parseContent(args, entry, messageArgs) | ||||||
|  |         break | ||||||
|  |       case 'rest': | ||||||
|  |         parseRest(args, entry, messageArgsNullableCopy) | ||||||
|  |         break | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return args | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseFlags( | ||||||
|  |   args: Record<string, unknown>, | ||||||
|  |   entry: Args, | ||||||
|  |   argsNullable: Array<string | null> | ||||||
|  | ): void { | ||||||
|  |   for (let i = 0; i < argsNullable.length; i++) { | ||||||
|  |     if (entry.flag === argsNullable[i]) { | ||||||
|  |       argsNullable[i] = null | ||||||
|  |       args[entry.name] = true | ||||||
|  |       break | ||||||
|  |     } else args[entry.name] = entry.defaultValue ?? false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseMention( | ||||||
|  |   args: Record<string, unknown>, | ||||||
|  |   entry: Args, | ||||||
|  |   argsNullable: Array<string | null> | ||||||
|  | ): void { | ||||||
|  |   const regex = mentionToRegex[entry.match] | ||||||
|  |   const index = argsNullable.findIndex( | ||||||
|  |     (x) => typeof x === 'string' && regex.test(x) | ||||||
|  |   ) | ||||||
|  |   const regexMatches = regex.exec(argsNullable[index]!) | ||||||
|  |   args[entry.name] = | ||||||
|  |     regexMatches !== null | ||||||
|  |       ? regexMatches[0].replace(regex, '$1') | ||||||
|  |       : entry.defaultValue | ||||||
|  |   argsNullable[index] = null | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseContent( | ||||||
|  |   args: Record<string, unknown>, | ||||||
|  |   entry: Args, | ||||||
|  |   argsNonNullable: Array<string | null> | ||||||
|  | ): void { | ||||||
|  |   args[entry.name] = | ||||||
|  |     argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function parseRest( | ||||||
|  |   args: Record<string, unknown>, | ||||||
|  |   entry: Args, | ||||||
|  |   argsNullable: Array<string | null> | ||||||
|  | ): void { | ||||||
|  |   const restValues = argsNullable.filter((x) => typeof x === 'string') | ||||||
|  |   args[entry.name] = | ||||||
|  |     restValues !== null ? restValues?.join(' ') : entry.defaultValue | ||||||
|  | } | ||||||
							
								
								
									
										150
									
								
								test/argsparser_test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								test/argsparser_test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,150 @@ | ||||||
|  | import { Args, parseArgs } from '../src/utils/command.ts' | ||||||
|  | import { | ||||||
|  |   assertEquals, | ||||||
|  |   assertNotEquals | ||||||
|  | } from 'https://deno.land/std@0.95.0/testing/asserts.ts' | ||||||
|  | 
 | ||||||
|  | const commandArgs: Args[] = [ | ||||||
|  |   { | ||||||
|  |     name: 'originalMessage', | ||||||
|  |     match: 'content' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'permaban', | ||||||
|  |     match: 'flag', | ||||||
|  |     flag: '--permanent', | ||||||
|  |     defaultValue: true | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'user', | ||||||
|  |     match: 'mentionUser' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'reason', | ||||||
|  |     match: 'rest', | ||||||
|  |     defaultValue: 'ree' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | const messageArgs1: string[] = [ | ||||||
|  |   '<@!708544768342229012>', | ||||||
|  |   '--permanent', | ||||||
|  |   'bye', | ||||||
|  |   'bye', | ||||||
|  |   'Skyler' | ||||||
|  | ] | ||||||
|  | const expectedResult1 = { | ||||||
|  |   originalMessage: [ | ||||||
|  |     '<@!708544768342229012>', | ||||||
|  |     '--permanent', | ||||||
|  |     'bye', | ||||||
|  |     'bye', | ||||||
|  |     'Skyler' | ||||||
|  |   ], | ||||||
|  |   permaban: true, | ||||||
|  |   user: '708544768342229012', | ||||||
|  |   reason: 'bye bye Skyler' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Deno.test({ | ||||||
|  |   only: false, | ||||||
|  |   name: 'parse command arguments 1 (assertEquals)', | ||||||
|  |   fn: () => { | ||||||
|  |     const result = parseArgs(commandArgs, messageArgs1) | ||||||
|  |     assertEquals(result, expectedResult1) | ||||||
|  |   }, | ||||||
|  |   sanitizeOps: true, | ||||||
|  |   sanitizeResources: true, | ||||||
|  |   sanitizeExit: true | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const messageArgs2: string[] = [ | ||||||
|  |   '<@!708544768342229012>', | ||||||
|  |   'bye', | ||||||
|  |   'bye', | ||||||
|  |   'Skyler' | ||||||
|  | ] | ||||||
|  | const expectedResult2 = { | ||||||
|  |   originalMessage: ['<@!708544768342229012>', 'bye', 'bye', 'Skyler'], | ||||||
|  |   permaban: true, | ||||||
|  |   user: '708544768342229012', | ||||||
|  |   reason: 'bye bye Skyler' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Deno.test({ | ||||||
|  |   name: 'parse command arguments 2 (assertEquals)', | ||||||
|  |   fn: () => { | ||||||
|  |     const result = parseArgs(commandArgs, messageArgs2) | ||||||
|  |     assertEquals(result, expectedResult2) | ||||||
|  |   }, | ||||||
|  |   sanitizeOps: true, | ||||||
|  |   sanitizeResources: true, | ||||||
|  |   sanitizeExit: true | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const messageArgs3: string[] = [ | ||||||
|  |   '<@!708544768342229012>', | ||||||
|  |   'bye', | ||||||
|  |   'bye', | ||||||
|  |   'Skyler' | ||||||
|  | ] | ||||||
|  | const expectedResult3 = { | ||||||
|  |   permaban: false, | ||||||
|  |   user: '708544768342229012', | ||||||
|  |   reason: 'bye bye Skyler' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Deno.test({ | ||||||
|  |   name: 'parse command arguments default value (assertNotEquals)', | ||||||
|  |   fn: () => { | ||||||
|  |     const result = parseArgs(commandArgs, messageArgs3) | ||||||
|  |     assertNotEquals(result, expectedResult3) | ||||||
|  |   }, | ||||||
|  |   sanitizeOps: true, | ||||||
|  |   sanitizeResources: true, | ||||||
|  |   sanitizeExit: true | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | const commandArgs2: Args[] = [ | ||||||
|  |   { | ||||||
|  |     name: 'user', | ||||||
|  |     match: 'mentionUser' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'channel', | ||||||
|  |     match: 'mentionChannel' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'role', | ||||||
|  |     match: 'mentionRole' | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'reason', | ||||||
|  |     match: 'rest', | ||||||
|  |     defaultValue: 'ree' | ||||||
|  |   } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | const messageArgs4: string[] = [ | ||||||
|  |   '<@!708544768342229012>', | ||||||
|  |   'bye', | ||||||
|  |   '<#783319033730564098>', | ||||||
|  |   '<@&836715188690092032>' | ||||||
|  | ] | ||||||
|  | const expectedResult4 = { | ||||||
|  |   channel: '783319033730564098', | ||||||
|  |   role: '836715188690092032', | ||||||
|  |   user: '708544768342229012', | ||||||
|  |   reason: 'bye' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Deno.test({ | ||||||
|  |   name: 'parse command arguments mentions (assertEquals)', | ||||||
|  |   fn: () => { | ||||||
|  |     const result = parseArgs(commandArgs2, messageArgs4) | ||||||
|  |     assertEquals(result, expectedResult4) | ||||||
|  |   }, | ||||||
|  |   sanitizeOps: true, | ||||||
|  |   sanitizeResources: true, | ||||||
|  |   sanitizeExit: true | ||||||
|  | }) | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue