2020-04-09 23:08:28 +02:00
const { stripIndents } = require ( 'common-tags' ) ;
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
class CommandHandler extends Observer {
constructor ( client ) {
super ( client , {
name : 'commandHandler' ,
priority : 5 ,
guarded : true
} ) ;
this . client = client ;
this . hooks = [
[ 'message' , this . handleMessage . bind ( this ) ]
] ;
2020-04-16 14:37:04 +02:00
this . _startQuotes = Object . keys ( Constants . QuotePairs ) ;
this . _quoteMarks = this . _startQuotes + Object . values ( Constants . QuotePairs )
. join ( '' ) ;
2020-04-09 23:08:28 +02:00
}
async handleMessage ( message ) {
if ( ! this . client . _built
|| message . webhookID
|| message . author . bot
2020-04-19 21:53:59 +02:00
|| ( message . guild && ( ! message . guild . available || message . guild . banned ) ) ) return undefined ;
2020-04-09 23:08:28 +02:00
2020-04-19 21:53:59 +02:00
if ( message . guild ) {
if ( ! message . member ) await message . guild . members . fetch ( message . author . id ) ;
2020-04-09 23:08:28 +02:00
}
const content = message . content ;
const args = content . split ( ' ' ) ;
const { command , newArgs } = await this . _getCommand ( message , args ) ;
if ( ! command ) return undefined ;
message . command = command ;
2020-04-16 14:37:04 +02:00
return await this . handleCommand ( message , newArgs ) ;
2020-04-09 23:08:28 +02:00
}
async _getCommand ( message , [ arg1 , arg2 , ... args ] ) {
2020-05-06 09:11:00 +02:00
if ( message . guild ) await message . guild . settings ( ) ;
const prefix = message . guild ? . prefix || this . client . _options . bot . prefix ;
2020-04-09 23:08:28 +02:00
let command = null ;
let remains = [ ] ;
if ( arg1 && arg1 . startsWith ( prefix ) ) {
2020-05-06 01:40:46 +02:00
const commandName = arg1 . slice ( prefix . length ) ;
2020-04-09 23:08:28 +02:00
command = await this . _matchCommand ( message , commandName ) ;
remains = [ arg2 , ... args ] ;
} else if ( arg1 && arg2 && arg1 . startsWith ( '<@' ) ) {
const pattern = new RegExp ( ` ^(<@!? ${ this . client . user . id } >) ` , 'i' ) ;
if ( arg2 && pattern . test ( arg1 ) ) {
command = await this . _matchCommand ( message , arg2 ) ;
}
2020-04-17 17:23:13 +02:00
remains = args ;
2020-04-09 23:08:28 +02:00
}
return { command , newArgs : remains } ;
}
async _matchCommand ( message , commandName ) {
const command = this . client . resolver . components ( commandName , 'command' , true ) [ 0 ] ;
if ( ! command ) return null ;
2020-05-08 08:50:54 +02:00
message . _caller = commandName ; //Used for hidden commands as aliases.
2020-04-09 23:08:28 +02:00
//Eventually search for custom commands here.
return command ;
}
/* Command Handling */
async handleCommand ( message , args ) {
const inhibitor = await this . _handleInhibitors ( message ) ;
2020-04-17 17:23:13 +02:00
if ( inhibitor . error ) return this . _handleError ( { type : 'inhibitor' , info : inhibitor , message } ) ;
2020-04-09 23:08:28 +02:00
2020-05-21 12:47:58 +02:00
const response = await this . _parseArguments ( args , message . command . arguments , message . guild ) ;
if ( response . error ) {
return this . _handleError ( {
... response ,
message
} ) ;
} else {
message . parameters = message . command . parameterType === 'PLAIN' ? response . newArgs . join ( ' ' ) : response . newArgs ;
message . args = response . parsedArguments ;
}
2020-04-09 23:08:28 +02:00
2020-04-16 14:37:04 +02:00
const resolved = await message . resolve ( ) ;
if ( resolved . error ) {
2020-04-17 17:23:13 +02:00
this . client . logger . error ( ` Command Error | ${ message . command . resolveable } | Message ID: ${ message . id } \n ${ resolved . message } ` ) ;
2020-04-16 14:37:04 +02:00
return this . _handleError ( { type : 'command' , message } ) ;
}
2020-04-14 17:05:56 +02:00
2020-04-09 23:08: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 ( ) ) {
if ( inhibitor . guild && ! message . guild ) continue ;
promises . push ( ( async ( ) => {
2020-04-17 17:23:13 +02:00
let inhibited = inhibitor . execute ( message , message . command ) ;
2020-04-09 23:08:28 +02:00
if ( inhibited instanceof Promise ) inhibited = await inhibited ;
return inhibited ;
} ) ( ) ) ;
}
const reasons = ( await Promise . all ( promises ) ) . filter ( p => p . error ) ;
if ( reasons . length === 0 ) return { error : false } ;
reasons . sort ( ( a , b ) => b . inhibitor . priority - a . inhibitor . priority ) ;
return reasons [ 0 ] ;
}
2020-04-16 14:37:04 +02:00
async _handleError ( { type , message , info } ) {
const errorMessages = {
command : ( message ) => {
return stripIndents ` The command ** ${ message . command . moduleResolveable } ** had issues running. ** \` [ ${ message . id } ] \` **
2020-04-17 17:23:13 +02:00
Contact * * the bot owner ( s ) * * about this issue . You can also find support here : < $ { this . client . _options . bot . invite } > ` ;
2020-04-16 14:37:04 +02:00
} ,
inhibitor : ( message , info ) => {
2020-04-17 17:23:13 +02:00
return ` ${ info . message } ** \` [ ${ info . inhibitor . resolveable } ] \` ** ` ;
2020-04-16 14:37:04 +02:00
} ,
2020-04-17 17:23:13 +02:00
argument : ( message , { argument , missing } ) => {
return stripIndents ` The argument ** ${ argument . name } : ${ argument . type . toLowerCase ( ) } ** is required and ${ missing ? "was not provided." : "did not meet the requirements." } Expecting a \` ${ argument . type . toLowerCase ( ) } \` value. ` ;
2020-04-16 14:37:04 +02:00
}
2020-04-17 17:23:13 +02:00
} ;
2020-04-16 14:37:04 +02:00
await message . respond ( errorMessages [ type ] ( message , info ) , { emoji : 'failure' } ) ;
}
2020-05-22 22:13:47 +02:00
async _parseArguments ( args = [ ] , passedArguments = [ ] , guild = null , debug = false ) { //Only need guild parameter if using a resolver type in your arguments e.g. channel, user, member, role
2020-04-09 23:08:28 +02:00
2020-04-16 14:37:04 +02:00
args = this . _getWords ( args . join ( ' ' ) ) . map ( w => w [ 0 ] ) ;
2020-05-21 12:47:58 +02:00
const { shortFlags , longFlags , keys } = await this . _createFlags ( passedArguments ) ;
2020-04-11 15:56:52 +02:00
const regex = new RegExp ( ` ([0-9]*)( ${ Object . keys ( longFlags ) . map ( k => escapeRegex ( k ) ) . join ( '|' ) } )([0-9]*) ` , 'i' ) ;
2020-05-08 08:50:54 +02:00
2020-05-23 10:11:14 +02:00
//console.log('args')
//console.log(args)
2020-04-16 14:37:04 +02:00
let parsedArguments = [ ] ;
2020-04-11 15:56:52 +02:00
let params = [ ] ;
2020-04-16 14:37:04 +02:00
let currentArgument = null ;
2020-04-11 15:56:52 +02:00
for ( let i = 0 ; i < args . length ; i ++ ) {
const word = args [ i ] ;
2020-05-22 22:13:47 +02:00
if ( debug ) {
console . log ( "parsing word" , word )
console . log ( "parsing word value" , currentArgument ? . value ) ;
}
2020-04-11 15:56:52 +02:00
if ( ! word ) continue ;
2020-04-11 10:10:52 +02:00
const [ one , two , ... chars ] = word . split ( '' ) ;
2020-04-11 15:56:52 +02:00
if ( one === '-' && two !== '-' ) {
2020-04-11 12:00:53 +02:00
const name = [ two , ... chars ] . join ( '' ) ;
2020-05-06 01:40:46 +02:00
if ( ! keys . includes ( name ) ) {
params . push ( word ) ;
continue ;
}
2020-04-11 12:00:53 +02:00
currentArgument = shortFlags [ name ] ;
2020-04-16 14:37:04 +02:00
if ( currentArgument . type === 'BOOLEAN' ) {
currentArgument . value = currentArgument . default ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
2020-04-11 15:56:52 +02:00
if ( currentArgument . required && ! args [ i + 1 ] ) {
2020-05-21 12:47:58 +02:00
return {
error : true ,
type : 'argument' ,
info : { argument : currentArgument , word , missing : true }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: true }, message });
2020-04-11 12:00:53 +02:00
}
2020-04-11 15:56:52 +02:00
continue ;
2020-05-06 01:40:46 +02:00
} else if ( ( one === '-' && two === '-' ) || ( one === '—' ) ) { //Handling for "long dash" on mobile phones x_x
2020-04-11 15:56:52 +02:00
const name = one === '—'
? [ two , ... chars ] . join ( '' ) . toLowerCase ( )
: chars . join ( '' ) . toLowerCase ( ) ; //can convert to lowercase now that shortFlags are out of the way.
2020-05-06 01:40:46 +02:00
if ( ! keys . includes ( name ) ) {
params . push ( word ) ;
continue ;
}
2020-04-11 10:10:52 +02:00
currentArgument = longFlags [ name ] ;
2020-04-16 14:37:04 +02:00
if ( currentArgument . type === 'BOOLEAN' ) {
currentArgument . value = currentArgument . default ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
2020-04-11 15:56:52 +02:00
if ( currentArgument . required && ! args [ i + 1 ] ) {
2020-05-21 12:47:58 +02:00
return {
error : true ,
type : 'argument' ,
info : { argument : currentArgument , word , missing : true }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: true }, message });
2020-04-11 15:56:52 +02:00
}
continue ;
2020-04-11 12:00:53 +02:00
} else {
2020-04-11 10:10:52 +02:00
let match = regex . exec ( word ) ;
2020-05-23 10:11:14 +02:00
//console.log(match)
2020-04-11 15:56:52 +02:00
if ( match && match [ 2 ] ) {
currentArgument = longFlags [ match [ 2 ] ] ;
if ( params . length > 0 && [ 'INTEGER' , 'FLOAT' ] . includes ( currentArgument . type ) ) { //15 pts
const lastItem = params [ params . length - 1 ] ;
2020-05-21 12:47:58 +02:00
const beforeError = await this . _handleTypeParsing ( currentArgument , lastItem , guild ) ;
2020-04-11 15:56:52 +02:00
if ( beforeError ) {
continue ;
} else {
params . pop ( ) ;
currentArgument . value = lastItem ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
}
const value = match [ 1 ] || match [ 3 ] ;
2020-05-22 22:13:47 +02:00
if ( debug ) console . log ( "type parsing" , currentArgument . name , value )
2020-05-21 12:47:58 +02:00
const error = await this . _handleTypeParsing ( currentArgument , value , guild ) ;
2020-04-11 15:56:52 +02:00
if ( value ) {
if ( error ) {
if ( currentArgument . required ) {
2020-05-21 12:47:58 +02:00
return {
error : true ,
type : 'argument' ,
info : { argument : currentArgument , word , missing : false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
2020-04-11 15:56:52 +02:00
} else {
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
} else {
2020-04-14 17:05:56 +02:00
currentArgument . value = value ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
2020-04-11 15:56:52 +02:00
}
} else {
2020-05-22 22:13:47 +02:00
if ( debug ) console . log ( "whattttttttttttttt" ) ;
2020-04-11 15:56:52 +02:00
continue ;
}
} else {
2020-05-23 10:11:14 +02:00
//console.log(currentArgument?.name)
2020-04-11 15:56:52 +02:00
if ( currentArgument ) {
2020-05-22 22:13:47 +02:00
if ( debug ) console . log ( word ) ;
2020-05-21 12:47:58 +02:00
const error = await this . _handleTypeParsing ( currentArgument , word , guild ) ;
2020-05-23 10:11:14 +02:00
//console.log(error)
2020-04-11 15:56:52 +02:00
if ( error ) {
2020-04-16 14:37:04 +02:00
if ( currentArgument . default !== null ) {
2020-04-11 15:56:52 +02:00
params . push ( word ) ;
currentArgument . value = currentArgument . default ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
if ( currentArgument . required ) {
if ( currentArgument . infinite ) {
if ( currentArgument . value . length === 0 ) {
2020-05-21 12:47:58 +02:00
return {
error : true ,
type : 'argument' ,
info : { argument : currentArgument , word , missing : false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
2020-04-11 15:56:52 +02:00
} else {
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
params . push ( word ) ;
continue ;
}
} else {
2020-05-21 12:47:58 +02:00
return {
error : true ,
type : 'argument' ,
info : { argument : currentArgument , word , missing : false }
}
// return this._handleError({ type: 'argument', info: { argument: currentArgument, word, missing: false }, message });
2020-04-11 15:56:52 +02:00
}
} else {
currentArgument = null ;
params . push ( word ) ;
continue ;
}
} else {
if ( currentArgument . infinite ) continue ;
parsedArguments . push ( currentArgument ) ;
currentArgument = null ;
continue ;
}
} else {
2020-04-16 14:37:04 +02:00
const lastArgument = parsedArguments [ parsedArguments . length - 1 ] ;
2020-04-17 17:23:13 +02:00
if ( lastArgument && lastArgument . type === 'BOOLEAN' && lastArgument . value === lastArgument . default ) {
2020-05-21 12:47:58 +02:00
const error = await this . _handleTypeParsing ( lastArgument , word , guild ) ;
2020-04-16 14:37:04 +02:00
if ( ! error ) continue ;
}
2020-04-11 15:56:52 +02:00
params . push ( word ) ;
continue ;
}
2020-04-11 12:00:53 +02:00
}
2020-04-09 23:08:28 +02:00
}
}
2020-05-08 08:50:54 +02:00
if ( currentArgument ) parsedArguments . push ( currentArgument ) ;
2020-04-21 19:56:31 +02:00
const blah = parsedArguments . filter ( a => a . requiredArgument && ! a . value ) ;
const missingArgument = blah [ 0 ] ;
2020-05-21 12:47:58 +02:00
if ( missingArgument ) return {
error : true ,
type : 'argument' ,
info : { argument : missingArgument , missing : true }
} ;
2020-04-21 19:56:31 +02:00
2020-04-17 17:23:13 +02:00
//fucking kill me
const fff = { } ;
parsedArguments . map ( a => fff [ a . name ] = a ) ;
return { parsedArguments : fff , newArgs : params } ;
2020-04-11 15:56:52 +02:00
2020-04-09 23:08:28 +02:00
}
2020-05-08 08:50:54 +02:00
async _handleTypeParsing ( argument , string , guild ) {
2020-04-11 12:00:53 +02:00
2020-05-08 08:50:54 +02:00
const parse = async ( argument , string , guild ) => {
2020-04-11 12:00:53 +02:00
2020-05-08 08:50:54 +02:00
const { error , value } = await this . constructor . parseType ( argument . type , string , this . client . resolver , guild ) ; //Cannot access static functions through "this".
2020-04-11 15:56:52 +02:00
if ( error ) return { error : true } ;
if ( [ 'INTEGER' , 'FLOAT' ] . includes ( argument . type ) ) {
const { min , max } = argument ;
if ( value > max && max !== null ) {
return { error : true } ;
}
if ( value < min && min !== null ) {
return { error : true } ;
}
2020-04-11 12:00:53 +02:00
}
2020-04-11 15:56:52 +02:00
2020-05-08 08:50:54 +02:00
if ( argument . options . length > 0 ) {
let found = null ;
for ( const option of argument . options ) {
if ( option !== value ) {
continue ;
} else {
found = option ;
}
}
if ( ! found ) return { error : true } ;
else return { error : false , value : found } ;
}
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-05-08 08:50:54 +02:00
const { error , value } = await parse ( argument , string , guild ) ;
2020-04-11 15:56:52 +02:00
2020-05-22 22:13:47 +02:00
if ( ! error && ( value !== undefined || value !== '' ) ) {
2020-04-11 15:56:52 +02:00
argument . infinite
? argument . value . push ( value )
: argument . value = value ;
}
return error ;
2020-04-11 12:00:53 +02:00
}
2020-04-09 23:08:28 +02:00
async _createFlags ( args ) {
let shortFlags = { } ;
let longFlags = { } ;
let keys = [ ] ;
2020-05-08 08:50:54 +02:00
for ( let arg of args ) {
arg = new Argument ( this . client , arg )
2020-04-09 23:08:28 +02:00
let letters = [ ] ;
let names = [ arg . name , ... arg . aliases ] ;
keys = [ ... keys , ... names ] ;
for ( const name of names ) {
2020-04-11 10:10:52 +02:00
longFlags [ 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 ;
if ( keys . includes ( letter ) ) letter = letter . toUpperCase ( ) ;
if ( keys . includes ( letter ) ) break ;
keys . push ( letter ) ;
letters . push ( letter ) ;
2020-04-11 10:10:52 +02:00
shortFlags [ letter ] = arg ;
2020-04-09 23:08:28 +02:00
}
}
return { shortFlags , longFlags , keys } ;
}
2020-04-16 14:37:04 +02:00
_getWords ( string = '' ) {
let quoted = false ,
wordStart = true ,
startQuote = '' ,
endQuote = false ,
isQuote = false ,
word = '' ,
words = [ ] ,
chars = string . split ( '' ) ;
chars . forEach ( ( char ) => {
if ( /\s/ . test ( char ) ) {
if ( endQuote ) {
quoted = false ;
endQuote = false ;
isQuote = true ;
}
if ( quoted ) {
word += char ;
} else if ( word !== '' ) {
words . push ( [ word , isQuote ] ) ;
isQuote = false ;
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 ;
}
} else if ( wordStart && this . _startQuotes . includes ( char ) ) {
quoted = true ;
startQuote = char ;
} else {
word += char ;
}
} else {
if ( endQuote ) {
word += endQuote ;
endQuote = false ;
}
word += char ;
wordStart = false ;
}
} ) ;
if ( endQuote ) {
words . push ( [ word , true ] ) ;
} else {
word . split ( /\s/ ) . forEach ( ( subWord , i ) => {
if ( i === 0 ) {
words . push ( [ startQuote + subWord , false ] ) ;
} else {
words . push ( [ subWord , false ] ) ;
}
} ) ;
}
return words ;
}
2020-05-21 12:47:58 +02:00
static async parseType ( type , str , resolver , guild ) {
2020-04-11 12:00:53 +02:00
//INTEGER AND FLOAT ARE SAME FUNCTION
const types = {
STRING : ( str ) => {
return { error : false , value : ` ${ str } ` } ;
} ,
INTEGER : ( str ) => {
const int = parseInt ( str ) ;
2020-04-17 17:23:13 +02:00
if ( Math . round ( int ) !== int ) return { error : true } ;
2020-05-21 12:47:58 +02:00
if ( isNaN ( int ) ) return { error : true } ;
2020-04-11 12:00:53 +02:00
return { error : false , value : int } ;
} ,
FLOAT : ( str ) => {
const float = parseInt ( str ) ;
2020-05-21 12:47:58 +02:00
if ( isNaN ( float ) ) return { error : true } ;
2020-04-11 12:00:53 +02:00
return { error : false , value : float } ;
} ,
BOOLEAN : ( str ) => {
const truthy = [ 'yes' , 'y' , 'true' , 't' , 'on' , 'enable' ] ;
const falsey = [ 'no' , 'n' , 'false' , 'f' , 'off' , 'disable' ] ;
if ( typeof str === 'boolean' ) return { error : false , value : str } ;
if ( typeof str === 'string' ) str = str . toLowerCase ( ) ;
if ( truthy . includes ( str ) ) return { error : false , value : true } ;
if ( falsey . includes ( str ) ) return { error : false , value : false } ;
return { error : true } ;
2020-04-21 19:56:31 +02:00
} ,
USER : ( str ) => { //eslint-disable-line no-unused-vars
} ,
MEMBER : ( str ) => { //eslint-disable-line no-unused-vars
} ,
2020-05-08 08:50:54 +02:00
CHANNEL : async ( str , resolver , guild ) => { //eslint-disable-line no-unused-vars
const channels = await resolver . resolveChannels ( str , guild , true ) ;
if ( channels . length === 0 ) return { error : true } ;
else return { error : false , value : channels [ 0 ] }
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-04-11 15:56:52 +02:00
2020-05-08 08:50:54 +02:00
return await types [ type ] ( str , resolver , guild ) ;
2020-04-11 12:00:53 +02:00
}
2020-04-09 23:08:28 +02:00
}
2020-04-16 14:37:04 +02:00
module . exports = CommandHandler ;
const Constants = {
QuotePairs : {
'"' : '"' ,
"'" : "'" ,
'‘ ' : '’ '
}
2020-04-17 17:23:13 +02:00
} ;