diff --git a/mod.ts b/mod.ts index f2a0842..57f6488 100644 --- a/mod.ts +++ b/mod.ts @@ -191,3 +191,4 @@ export { isVoiceChannel, default as getChannelByType } from './src/utils/channel.ts' +export * from "./src/utils/command.ts" \ No newline at end of file diff --git a/src/commands/client.ts b/src/commands/client.ts index 22b0117..9442f4f 100644 --- a/src/commands/client.ts +++ b/src/commands/client.ts @@ -9,6 +9,7 @@ import { CommandsManager, parseCommand } from './command.ts' +import { parseArgs } from '../utils/command.ts' import { Extension, ExtensionsManager } from './extension.ts' type PrefixReturnType = string | string[] | Promise @@ -239,7 +240,7 @@ export class CommandClient extends Client implements CommandClientOptions { client: this, name: parsed.name, prefix, - args: parsed.args, + args: parseArgs(command.args, parsed.args), argString: parsed.argString, message: msg, author: msg.author, diff --git a/src/commands/command.ts b/src/commands/command.ts index 987eaee..8601d70 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -6,7 +6,7 @@ import { Collection } from '../utils/collection.ts' import type { CommandClient } from './client.ts' import type { Extension } from './extension.ts' import { join, walk } from '../../deps.ts' - +import type { Args } from '../utils/command.ts' export interface CommandContext { /** The Client object */ client: CommandClient @@ -23,7 +23,7 @@ export interface CommandContext { /** Name of Command which was used */ name: string /** Array of Arguments used with Command */ - args: string[] + args: Record | null /** Complete Raw String of Arguments */ argString: string /** Guild which the command has called */ @@ -46,7 +46,7 @@ export interface CommandOptions { /** Usage Example of Command, only Arguments (without Prefix and Name) */ examples?: string | string[] /** 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?: string | string[] /** Permission(s) required for using Command */ @@ -81,7 +81,7 @@ export class Command implements CommandOptions { extension?: Extension usage?: string | string[] examples?: string | string[] - args?: number | boolean | string[] + args?: Args[] permissions?: string | string[] userPermissions?: string | string[] botPermissions?: string | string[] diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000..4eda5bc --- /dev/null +++ b/src/utils/command.ts @@ -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 { + name: string + match: CommandArgumentMatchTypes + defaultValue?: T + flag?: string +} + +export function parseArgs( + commandArgs: Args[] | undefined, + messageArgs: string[] +): Record | null { + if (commandArgs === undefined) return null + + const messageArgsNullableCopy: Array = [...messageArgs] + const args: Record = {} + + 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, + entry: Args, + argsNullable: Array +): 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, + entry: Args, + argsNullable: Array +): 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, + entry: Args, + argsNonNullable: Array +): void { + args[entry.name] = + argsNonNullable.length > 0 ? argsNonNullable : entry.defaultValue +} + +function parseRest( + args: Record, + entry: Args, + argsNullable: Array +): void { + const restValues = argsNullable.filter((x) => typeof x === 'string') + args[entry.name] = + restValues !== null ? restValues?.join(' ') : entry.defaultValue +} diff --git a/test/argsparser_test.ts b/test/argsparser_test.ts new file mode 100644 index 0000000..f428246 --- /dev/null +++ b/test/argsparser_test.ts @@ -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 +})