2020-04-11 15:56:52 +02:00
const escapeRegex = require ( 'escape-string-regexp' ) ;
2020-04-09 23:08:28 +02:00
2020-05-08 08:50:54 +02:00
const { Argument , Observer } = require ( '../../../interfaces/' ) ;
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
const Constants = {
QuotePairs : {
'"' : '"' , //regular double
"'" : "'" , //regular single
"‘ " : "’ " , //smart single
"“" : "”" //smart double
}
} ;
2020-04-09 23:08:28 +02:00
class CommandHandler extends Observer {
constructor ( client ) {
super ( client , {
name : 'commandHandler' ,
priority : 5 ,
2020-06-02 12:09:28 +02:00
guarded : true
2020-04-09 23:08:28 +02:00
} ) ;
this . client = client ;
this . hooks = [
[ 'message' , this . handleMessage . bind ( this ) ]
] ;
2020-06-02 12:09:28 +02:00
this . _startQuotes = Object . keys ( Constants . QuotePairs ) ; //Used for the "getQuotes" function. Parses arguments with quotes in it to combine them into one array argument.
this . _quoteMarks = this . _startQuotes + Object . values ( Constants . QuotePairs ) // ^
2020-04-16 14:37:04 +02:00
. join ( '' ) ;
2020-04-09 23:08:28 +02:00
}
async handleMessage ( message ) {
if ( ! this . client . _built
|| message . webhookID
|| message . author . bot
2020-06-02 12:09:28 +02:00
|| message . guild && ( ! message . guild . available || message . guild . banned ) ) return undefined ;
//Don't parse commands if client isn't built (commands aren't loaded), is a webhook, bot, or if the guild isn't available/is banned.
2020-04-09 23:08:28 +02:00
2020-04-19 21:53:59 +02:00
if ( message . guild ) {
2020-06-02 12:09:28 +02:00
if ( ! message . member ) await message . guild . members . fetch ( message . author . id ) ; //Fetch member if discord.js doesn't fetch them automatically.
2020-04-09 23:08:28 +02:00
}
2020-06-02 12:09:28 +02:00
const { command , parameters } = await this . _getCommand ( message ) ;
2020-04-09 23:08:28 +02:00
if ( ! command ) return undefined ;
message . command = command ;
2020-06-02 12:09:28 +02:00
// const timestamp1 = new Date().getTime();
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
const response = await this . _parseArguments ( parameters , command . arguments , message . guild ) ;
2020-05-21 12:47:58 +02:00
if ( response . error ) {
2020-06-02 12:09:28 +02:00
return this . handleError ( message , { type : 'argument' , ... response } ) ;
2020-06-04 19:59:09 +02:00
}
2020-06-02 12:09:28 +02:00
if ( command . keepQuotes ) {
2020-06-04 19:59:09 +02:00
message . parameters = response . parameters . map ( ( [ param , isQuote ] ) => ( isQuote ? ` " ${ param } " ` : param ) ) ;
} else {
message . parameters = response . parameters . map ( ( p ) => p [ 0 ] ) ;
2020-04-16 14:37:04 +02:00
}
2020-04-14 17:05:56 +02:00
2020-06-02 12:09:28 +02:00
// const timestamp2 = new Date().getTime();
// this.client.logger.debug(`Client took ${timestamp2-timestamp1}ms to parse arguments.`);
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
message . arguments = response . args ;
return this . handleCommand ( message , response . parameters ) ;
2020-04-09 23:08:28 +02:00
}
2020-06-02 12:09:28 +02:00
async _parseArguments ( parameters = [ ] , args = [ ] , guild = null ) {
2020-04-16 14:37:04 +02:00
2020-06-02 12:09:28 +02:00
parameters = this . _getQuotes ( parameters . join ( ' ' ) ) ;
const { shortArgs , longArgs } = await this . _createArguments ( args ) ;
const regex = new RegExp ( ` ([0-9]*)( ${ Object . keys ( longArgs ) . sort ( ( a , b ) => b . length - a . length )
. map ( ( k ) => escapeRegex ( k ) )
. join ( '|' ) } ) ( [ 0 - 9 ] * ) ` , 'iu');
const findArgument = ( word ) => {
const matches = regex . exec ( word ) ;
const [ one , two , ... chars ] = word . split ( '' ) ;
let arg = null ,
value = null ;
if ( one === '-' && two !== '-' ) {
const name = [ two , ... chars ] . join ( '' ) ;
const shortFlag = shortArgs [ name ] ;
if ( shortFlag ) {
arg = shortFlag ;
arg . _flag = true ;
}
} else if ( one === '-' && two === '-' || one === '—' && two !== '—' ) {
const name = one === '—'
? [ two , ... chars ] . join ( '' )
: chars . join ( '' ) ;
const longFlag = longArgs [ name ] ;
if ( longFlag ) {
arg = longFlag ;
arg . _flag = true ;
}
} else if ( matches && ( matches [ 1 ] || matches [ 3 ] ) ) {
const [ , , name ] = matches ;
const number = matches [ 1 ] || matches [ 3 ] ;
2020-06-04 19:59:09 +02:00
const argument = longArgs [ name ] ;
if ( argument && argument . types . includes ( 'VERBAL' ) ) {
arg = longArgs [ name ] ;
value = number ;
}
2020-06-02 12:09:28 +02:00
} else if ( longArgs [ word . toLowerCase ( ) ] ) {
2020-06-04 19:59:09 +02:00
const argument = longArgs [ word . toLowerCase ( ) ] ;
if ( argument && argument . types . includes ( 'VERBAL' ) ) {
arg = longArgs [ word . toLowerCase ( ) ] ;
}
2020-04-16 14:37:04 +02:00
}
2020-06-02 12:09:28 +02:00
return { arg , value } ;
2020-04-17 17:23:13 +02:00
} ;
2020-04-16 14:37:04 +02:00
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
const newParameters = [ ] ,
newArguments = [ ] ;
2020-04-16 14:37:04 +02:00
2020-06-02 12:09:28 +02:00
const lookBehind = async ( argument ) => { //Checks previous argument for an integer or float value, "15 points".
let response = { } ;
if ( newParameters . length > 0 && [ 'INTEGER' , 'FLOAT' ] . includes ( argument . type ) ) {
2020-06-04 19:59:09 +02:00
const [ lastWord ] = newParameters [ newParameters . length - 1 ] ;
response = await this . _parseArgumentType ( argument , lastWord , guild ) ;
2020-06-02 12:09:28 +02:00
if ( ! response . error ) {
newParameters . pop ( ) ; //Deletes latest parameter.
newArguments . push ( argument ) ; //Adds argument with value of the latest parameter.
return { error : false } ;
}
}
return { error : true , ... response } ;
} ;
let error = null ,
currentArgument = null ;
2020-06-04 19:59:09 +02:00
2020-06-02 12:09:28 +02:00
for ( let i = 0 ; i < parameters . length ; i ++ ) {
2020-06-04 19:59:09 +02:00
const [ word , isQuote ] = parameters [ i ] ;
2020-06-02 12:09:28 +02:00
if ( currentArgument ) { //One of the previous words had an argument, trying to parse the type until error.
let response = await this . _parseArgumentType ( currentArgument , word , guild ) ;
if ( response . error ) {
if ( response . force ) { //Overrides error if min/max (stupid)
error = { argument : currentArgument , ... response } ;
break ;
2020-04-11 15:56:52 +02:00
}
2020-06-02 12:09:28 +02:00
const behind = await lookBehind ( currentArgument ) ; //Check for "15 points" (lookbehind the argument)
if ( ! behind . error ) {
if ( ! currentArgument . infinite ) currentArgument = null ;
} else {
if ( currentArgument . required ) {
if ( currentArgument . empty ) {
if ( behind . force ) response = behind ; //Overrides error if min/max (stupid)
error = { argument : currentArgument , ... response } ;
break ;
2020-04-11 15:56:52 +02:00
}
2020-06-02 12:09:28 +02:00
} else if ( currentArgument . empty ) {
currentArgument . setDefault ( ) ;
2020-04-11 15:56:52 +02:00
}
2020-06-02 12:09:28 +02:00
newArguments . push ( currentArgument ) ;
currentArgument = null ;
2020-04-11 15:56:52 +02:00
}
2020-06-04 19:59:09 +02:00
newParameters . push ( [ word , isQuote ] ) ;
2020-04-11 15:56:52 +02:00
} else {
2020-06-02 12:09:28 +02:00
newArguments . push ( currentArgument ) ;
if ( ! currentArgument . infinite ) currentArgument = null ;
}
} else { //Trying to find a new argument to parse or add as a parameter.
const { arg , value } = findArgument ( word ) ;
let alreadyMatched = false ;
for ( const { name } of newArguments ) {
if ( arg && arg . name === name ) alreadyMatched = true ;
}
if ( arg && ! alreadyMatched ) {
if ( value ) { //Pre-matched value (15pts/pts15) found by regex, won't need a separate word to parse the argument.
const response = await this . _parseArgumentType ( arg , value , guild ) ;
if ( arg . required ) {
if ( response . error ) {
error = { argument : arg , ... response } ;
break ;
2020-04-11 15:56:52 +02:00
}
}
2020-06-02 12:09:28 +02:00
newArguments . push ( arg ) ;
if ( arg . infinite ) currentArgument = arg ;
2020-04-11 15:56:52 +02:00
} else {
2020-06-02 12:09:28 +02:00
currentArgument = arg ;
2020-04-11 15:56:52 +02:00
}
2020-06-02 12:09:28 +02:00
} else {
2020-06-04 19:59:09 +02:00
newParameters . push ( [ word , isQuote ] ) ;
2020-04-11 12:00:53 +02:00
}
2020-04-09 23:08:28 +02:00
}
}
2020-06-02 12:09:28 +02:00
if ( error ) return error ;
2020-04-21 19:56:31 +02:00
2020-06-02 12:09:28 +02:00
if ( currentArgument ) { //Add the last argument awaiting to be parsed (argument was left hanging at the end w/o a value)
const behind = await lookBehind ( currentArgument ) ;
if ( ! behind . error ) {
newArguments . push ( currentArgument ) ;
} else {
if ( currentArgument . empty ) {
if ( currentArgument . required ) {
if ( behind . force ) return { argument : currentArgument , ... behind } ;
return {
index : 'COMMANDHANDLER_TYPE_ERROR' ,
argument : currentArgument ,
error : true
} ;
}
currentArgument . setDefault ( ) ;
}
newArguments . push ( currentArgument ) ;
}
}
2020-04-21 19:56:31 +02:00
2020-06-02 12:09:28 +02:00
const object = { } ;
newArguments . map ( ( a ) => object [ a . name ] = a ) ; //eslint-disable-line no-return-assign
2020-04-17 17:23:13 +02:00
2020-06-02 12:09:28 +02:00
return { parameters : newParameters , args : object , error : false } ;
2020-04-11 15:56:52 +02:00
2020-04-09 23:08:28 +02:00
}
2020-06-02 12:09:28 +02:00
async _parseArgumentType ( argument , string , guild ) { //Parsing argument values to make sure they're correct.
const parse = async ( argument , string , guild ) => {
2020-06-04 19:59:09 +02:00
let { error , value } = await this . parseType ( argument . type , string , guild ) ; //eslint-disable-line prefer-const
2020-06-02 12:09:28 +02:00
if ( error ) {
return {
index : 'COMMANDHANDLER_TYPE_ERROR' ,
error
} ;
}
2020-04-11 12:00:53 +02:00
2020-04-11 15:56:52 +02:00
if ( [ 'INTEGER' , 'FLOAT' ] . includes ( argument . type ) ) {
const { min , max } = argument ;
2020-06-02 12:09:28 +02:00
if ( max !== null && value > max ) {
if ( ! argument . ignoreInvalid ) return {
index : ` COMMANDHANDLER_NUMBERMAX_ERROR ` ,
args : { min , max } ,
force : true ,
error : true
} ;
value = argument . max ;
} else if ( min !== null && value < min ) {
if ( ! argument . ignoreInvalid ) return {
index : ` COMMANDHANDLER_NUMBERMIN_ERROR ` ,
args : { min , max } ,
force : true ,
error : true
} ;
value = argument . min ;
2020-04-11 15:56:52 +02:00
}
2020-04-11 12:00:53 +02:00
}
2020-04-11 15:56:52 +02:00
2020-06-02 12:09:28 +02:00
if ( argument . options . length > 0 && ! argument . options . includes ( value ) ) return {
index : ` COMMANDHANDLER_OPTIONS_ERROR ` ,
args : { options : argument . options . map ( ( o ) => ` \` ${ o } \` ` ) . join ( ', ' ) } ,
error : true
} ;
2020-05-08 08:50:54 +02:00
2020-04-11 15:56:52 +02:00
return { error : false , value } ;
2020-04-17 17:23:13 +02:00
} ;
2020-04-11 12:00:53 +02:00
2020-06-02 12:09:28 +02:00
const response = await parse ( argument , string , guild ) ;
2020-04-11 15:56:52 +02:00
2020-06-02 12:09:28 +02:00
const { value } = response ;
if ( value ) {
if ( argument . infinite ) argument . value . push ( value ) ;
else argument . value = value ;
} else {
argument . setDefault ( guild ) ;
}
2020-04-11 15:56:52 +02:00
2020-06-02 12:09:28 +02:00
if ( response . error ) {
return response ;
}
2020-04-11 15:56:52 +02:00
2020-06-02 12:09:28 +02:00
return response ;
2020-04-11 12:00:53 +02:00
}
2020-06-02 12:09:28 +02:00
async _createArguments ( args ) {
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
const shortArgs = { } ,
longArgs = { } ;
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
let argKeys = [ ] ;
2020-05-08 08:50:54 +02:00
for ( let arg of args ) {
2020-06-02 12:09:28 +02:00
arg = new Argument ( this . client , arg ) ;
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
const letters = [ ] ;
const names = [ arg . name , ... arg . aliases ] ;
argKeys = [ ... argKeys , ... names ] ;
2020-04-09 23:08:28 +02:00
for ( const name of names ) {
2020-06-02 12:09:28 +02:00
longArgs [ name ] = arg ;
2020-04-09 23:08:28 +02:00
if ( ! arg . types . includes ( 'FLAG' ) ) continue ;
let letter = name . slice ( 0 , 1 ) ;
if ( letters . includes ( letter ) ) continue ;
2020-06-02 12:09:28 +02:00
if ( argKeys . includes ( letter ) ) letter = letter . toUpperCase ( ) ;
if ( argKeys . includes ( letter ) ) {
this . client . logger . warn ( ` Command has too many arguments with the same first letter: ${ argKeys . join ( ', ' ) } ` ) ;
break ;
}
2020-04-09 23:08:28 +02:00
2020-06-02 12:09:28 +02:00
argKeys . push ( letter ) ;
2020-04-09 23:08:28 +02:00
letters . push ( letter ) ;
2020-06-02 12:09:28 +02:00
shortArgs [ letter ] = arg ;
2020-04-09 23:08:28 +02:00
}
}
2020-06-02 12:09:28 +02:00
return { shortArgs , longArgs } ;
}
async _getCommand ( message ) {
const [ arg1 , arg2 , ... args ] = message . content . split ( ' ' ) ;
if ( message . guild ) await message . guild . settings ( ) ;
const { prefix } = message ;
let command = null ,
remains = [ ] ;
if ( arg1 && arg1 . startsWith ( prefix ) ) {
const commandName = arg1 . slice ( prefix . length ) ; //Grabs the command name by slicing off the prefix.
command = await this . _matchCommand ( message , commandName ) ;
remains = [ arg2 , ... args ] ;
} else if ( arg1 && arg2 && arg1 . startsWith ( '<@' ) ) { //Checks if the first argument is a mention and if a command is after it.
const pattern = new RegExp ( ` ^(<@!? ${ this . client . user . id } >) ` , 'iu' ) ;
if ( arg2 && pattern . test ( arg1 ) ) {
command = await this . _matchCommand ( message , arg2 ) ;
}
remains = args ;
}
return { command , parameters : remains } ;
}
async _matchCommand ( message , commandName ) {
const [ command ] = this . client . resolver . components ( commandName , 'command' , true ) ;
if ( ! command ) return null ;
//Eventually search for custom commands here.
message . _caller = commandName ; //Used for hidden commands as aliases.
return command ;
}
/* Command Handling */
async handleCommand ( message ) {
const inhibitor = await this . _handleInhibitors ( message ) ;
if ( inhibitor . error ) return this . handleError ( message , { type : 'inhibitor' , ... inhibitor } ) ;
const resolved = await message . resolve ( ) ;
if ( resolved . error ) {
this . client . logger . error ( ` Command Error | ${ message . command . resolveable } | Message ID: ${ message . id } \n ${ resolved . message } ` ) ;
return this . handleError ( message , { type : 'command' } ) ;
}
return true ;
2020-04-09 23:08:28 +02:00
}
2020-06-02 12:09:28 +02:00
async _handleInhibitors ( message ) {
const inhibitors = this . client . registry . components . filter ( ( c ) => c . type === 'inhibitor' && ! c . disabled ) ;
if ( inhibitors . size === 0 ) return { error : false } ;
const promises = [ ] ;
for ( const inhibitor of inhibitors . values ( ) ) { // Loops through all inhibitors, executing them.
if ( inhibitor . guild && ! message . guild ) continue ;
promises . push ( ( async ( ) => {
let inhibited = inhibitor . execute ( message , message . command ) ;
if ( inhibited instanceof Promise ) inhibited = await inhibited ;
return inhibited ;
} ) ( ) ) ;
}
const reasons = ( await Promise . all ( promises ) ) . filter ( ( p ) => p . error ) ; // Filters out inhibitors with only errors.
if ( reasons . length === 0 ) return { error : false } ;
reasons . sort ( ( a , b ) => b . inhibitor . priority - a . inhibitor . priority ) ; // Sorts inhibitor errors by most important.
return reasons [ 0 ] ;
}
async parseType ( type , str , guild ) { //Types used for parsing argument types.
const types = {
STRING : ( str ) => ( { error : false , value : str } ) ,
INTEGER : ( str ) => {
const int = parseInt ( str ) ;
if ( Math . round ( int ) !== int ) return { error : true } ;
if ( isNaN ( int ) ) return { error : true } ;
return { error : false , value : int } ;
} ,
FLOAT : ( str ) => {
const float = parseInt ( str ) ;
if ( isNaN ( float ) ) return { error : true } ;
return { error : false , value : float } ;
} ,
BOOLEAN : ( str ) => {
const bool = this . client . resolver . resolveBoolean ( str ) ;
if ( bool === null ) return { error : true } ;
return { error : false , value : bool } ;
} ,
USER : async ( str ) => {
const user = await this . client . resolver . resolveUser ( str , true ) ;
if ( ! user ) return { error : true } ;
return { error : false , value : user } ;
} ,
MEMBER : async ( str , guild ) => {
const member = await this . client . resolver . resolveMember ( str , true , guild ) ;
if ( ! member ) return { error : true } ;
return { error : false , value : member } ;
} ,
TEXTCHANNEL : async ( str , guild ) => {
const channel = await this . client . resolver . resolveChannel ( str , true , guild , ( channel ) => channel . type === 'text' ) ;
if ( ! channel ) return { error : true } ;
return { error : false , value : channel } ;
} ,
VOICECHANNEL : async ( str , guild ) => {
const channel = await this . client . resolver . resolveChannel ( str , true , guild , ( channel ) => channel . type === 'voice' ) ;
if ( ! channel ) return { error : true } ;
return { error : false , value : channel } ;
} ,
CHANNEL : async ( str , guild ) => {
const channel = await this . client . resolver . resolveChannel ( str , true , guild ) ;
if ( ! channel ) return { error : true } ;
return { error : false , value : channel } ;
} ,
ROLE : async ( str , guild ) => {
const role = await this . client . resolver . resolveRole ( str , true , guild ) ;
if ( ! role ) return { error : true } ;
return { error : false , value : role } ;
2020-06-04 19:59:09 +02:00
} ,
TIME : async ( str ) => {
const time = await this . client . resolver . resolveTime ( str ) ;
if ( ! time ) return { error : true } ;
return { error : false , value : time } ;
2020-06-02 12:09:28 +02:00
}
} ;
return types [ type ] ( str , guild ) ;
}
/* Made by my friend Qwerasd#5202 */
// I'm not entirely sure how this works, except for the fact that it loops through each CHARACTER and tries to match quotes together.
// Supposedly quicker than regex, and I'd agree with that statement. Big, messy, but quick.
_getQuotes ( string ) {
2020-04-16 14:37:04 +02:00
let quoted = false ,
wordStart = true ,
startQuote = '' ,
endQuote = false ,
2020-06-04 19:59:09 +02:00
isQuote = false ,
2020-06-02 12:09:28 +02:00
word = '' ;
2020-06-04 19:59:09 +02:00
const words = [ ] ,
2020-04-16 14:37:04 +02:00
chars = string . split ( '' ) ;
chars . forEach ( ( char ) => {
2020-06-02 12:09:28 +02:00
if ( ( /\s/u ) . test ( char ) ) {
2020-04-16 14:37:04 +02:00
if ( endQuote ) {
quoted = false ;
endQuote = false ;
2020-06-04 19:59:09 +02:00
isQuote = true ;
2020-04-16 14:37:04 +02:00
}
if ( quoted ) {
word += char ;
} else if ( word !== '' ) {
2020-06-04 19:59:09 +02:00
words . push ( [ word , isQuote ] ) ;
isQuote = false ;
2020-04-16 14:37:04 +02:00
startQuote = '' ;
word = '' ;
wordStart = true ;
}
} else if ( this . _quoteMarks . includes ( char ) ) {
if ( endQuote ) {
word += endQuote ;
endQuote = false ;
}
if ( quoted ) {
if ( char === Constants . QuotePairs [ startQuote ] ) {
endQuote = char ;
} else {
word += char ;
}
2020-06-02 12:09:28 +02:00
} else if ( wordStart && this . _startQuotes . includes ( char ) ) {
2020-04-16 14:37:04 +02:00
quoted = true ;
startQuote = char ;
} else {
word += char ;
}
} else {
if ( endQuote ) {
word += endQuote ;
endQuote = false ;
}
word += char ;
wordStart = false ;
}
} ) ;
if ( endQuote ) {
2020-06-04 19:59:09 +02:00
words . push ( [ word , true ] ) ;
2020-04-16 14:37:04 +02:00
} else {
2020-06-02 12:09:28 +02:00
word . split ( /\s/u ) . forEach ( ( subWord , i ) => {
2020-04-16 14:37:04 +02:00
if ( i === 0 ) {
2020-06-04 19:59:09 +02:00
words . push ( [ startQuote + subWord , false ] ) ;
2020-04-16 14:37:04 +02:00
} else {
2020-06-04 19:59:09 +02:00
words . push ( [ subWord , false ] ) ;
2020-04-16 14:37:04 +02:00
}
} ) ;
}
return words ;
}
2020-06-02 12:09:28 +02:00
async handleError ( message , error ) { //Handle different types of errors
const messages = {
command : async ( ) => message . format ( 'COMMANDHANDLER_COMMAND_ERROR' , {
invite : this . client . _options . bot . invite ,
id : message . id ,
command : message . command . moduleResolveable
} ) ,
inhibitor : ( { inhibitor , args } ) => ` ${ message . format ( inhibitor . index , { command : message . command . moduleResolveable , ... args } )} ** \` [ ${ inhibitor . resolveable } ] \` ** ` ,
argument : ( { index , args , argument } ) => {
const type = message . format ( 'COMMANDHANDLER_TYPES' , { type : argument . type } , true ) ;
return message . format ( index , { type , arg : ` ${ message . command . name } : ${ argument . name } ` , ... args } ) ;
2020-04-11 12:00:53 +02:00
}
2020-04-17 17:23:13 +02:00
} ;
2020-04-11 12:00:53 +02:00
2020-06-02 12:09:28 +02:00
return message . respond ( await messages [ error . type ] ( error ) , { emoji : 'failure' } ) ;
2020-04-11 12:00:53 +02:00
}
2020-04-09 23:08:28 +02:00
}
2020-06-02 12:09:28 +02:00
module . exports = CommandHandler ;