2021-03-31 02:19:04 +00:00
import { parseVars } from "./lib" ;
2020-12-15 01:44:28 +00:00
import { Collection } from "discord.js" ;
2021-03-30 10:25:07 +00:00
import { Client , Message , TextChannel , DMChannel , NewsChannel , Guild , User , GuildMember } from "discord.js" ;
2020-12-15 01:44:28 +00:00
import { getPrefix } from "../core/structures" ;
2021-04-03 09:58:20 +00:00
import { SingleMessageOptions } from "./libd" ;
import { hasPermission , getPermissionLevel , getPermissionName } from "./permissions" ;
2020-10-15 09:23:24 +00:00
2021-03-30 10:25:07 +00:00
interface CommandMenu {
args : any [ ] ;
client : Client ;
message : Message ;
channel : TextChannel | DMChannel | NewsChannel ;
guild : Guild | null ;
author : User ;
member : GuildMember | null ;
}
2020-10-15 09:23:24 +00:00
interface CommandOptions {
2020-12-15 01:44:28 +00:00
description? : string ;
endpoint? : boolean ;
usage? : string ;
2021-03-30 10:54:52 +00:00
permission? : number ;
2020-12-15 01:44:28 +00:00
aliases? : string [ ] ;
2021-03-30 10:25:07 +00:00
run ? : ( ( $ : CommandMenu ) = > Promise < any > ) | string ;
2020-12-15 01:44:28 +00:00
subcommands ? : { [ key : string ] : Command } ;
user? : Command ;
number ? : Command ;
any ? : Command ;
2020-07-25 08:15:26 +00:00
}
2021-04-03 09:58:20 +00:00
enum TYPES {
2020-12-15 01:44:28 +00:00
SUBCOMMAND ,
USER ,
NUMBER ,
ANY ,
NONE
2020-10-15 09:23:24 +00:00
}
export default class Command {
2020-12-15 01:44:28 +00:00
public readonly description : string ;
public readonly endpoint : boolean ;
public readonly usage : string ;
2021-03-30 10:54:52 +00:00
public readonly permission : number ; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on.
2020-12-15 01:44:28 +00:00
public readonly aliases : string [ ] ; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName : string | null ; // If the command is an alias, what's the original name?
2021-03-30 10:25:07 +00:00
public run : ( ( $ : CommandMenu ) = > Promise < any > ) | string ;
2020-12-15 01:44:28 +00:00
public readonly subcommands : Collection < string , Command > ; // This is the final data structure you'll actually use to work with the commands the aliases point to.
public user : Command | null ;
public number : Command | null ;
public any : Command | null ;
constructor ( options? : CommandOptions ) {
this . description = options ? . description || "No description." ;
this . endpoint = options ? . endpoint || false ;
this . usage = options ? . usage || "" ;
2021-03-30 10:54:52 +00:00
this . permission = options ? . permission ? ? - 1 ;
2020-12-15 01:44:28 +00:00
this . aliases = options ? . aliases ? ? [ ] ;
this . originalCommandName = null ;
this . run = options ? . run || "No action was set on this command!" ;
this . subcommands = new Collection ( ) ; // Populate this collection after setting subcommands.
this . user = options ? . user || null ;
this . number = options ? . number || null ;
this . any = options ? . any || null ;
if ( options ? . subcommands ) {
const baseSubcommands = Object . keys ( options . subcommands ) ;
// Loop once to set the base subcommands.
2020-12-15 07:56:09 +00:00
for ( const name in options . subcommands ) this . subcommands . set ( name , options . subcommands [ name ] ) ;
2020-12-15 01:44:28 +00:00
// Then loop again to make aliases point to the base subcommands and warn if something's not right.
// This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object.
for ( const name in options . subcommands ) {
const subcmd = options . subcommands [ name ] ;
subcmd . originalCommandName = name ;
const aliases = subcmd . aliases ;
for ( const alias of aliases ) {
if ( baseSubcommands . includes ( alias ) )
2021-03-30 08:58:21 +00:00
console . warn (
2020-12-15 01:44:28 +00:00
` " ${ alias } " in subcommand " ${ name } " was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.) `
) ;
else if ( this . subcommands . has ( alias ) )
2021-03-30 08:58:21 +00:00
console . warn (
2020-12-15 01:44:28 +00:00
` Duplicate alias " ${ alias } " at subcommand " ${ name } "! (Look at the next "Loading Command" line to see which command is affected.) `
) ;
else this . subcommands . set ( alias , subcmd ) ;
}
}
}
if ( this . user && this . user . aliases . length > 0 )
2021-03-30 08:58:21 +00:00
console . warn (
2020-12-15 01:44:28 +00:00
` There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.) `
2020-10-15 09:23:24 +00:00
) ;
2020-12-15 01:44:28 +00:00
if ( this . number && this . number . aliases . length > 0 )
2021-03-30 08:58:21 +00:00
console . warn (
2020-12-15 01:44:28 +00:00
` There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.) `
) ;
if ( this . any && this . any . aliases . length > 0 )
2021-03-30 08:58:21 +00:00
console . warn (
2020-12-15 01:44:28 +00:00
` There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.) `
2020-10-15 09:23:24 +00:00
) ;
}
2021-03-30 10:25:07 +00:00
public execute ( $ : CommandMenu ) {
if ( typeof this . run === "string" ) {
2020-12-15 01:44:28 +00:00
$ . channel . send (
parseVars (
2021-03-30 10:25:07 +00:00
this . run ,
2020-12-15 01:44:28 +00:00
{
author : $.author.toString ( ) ,
prefix : getPrefix ( $ . guild )
} ,
"???"
)
) ;
2021-03-30 10:25:07 +00:00
} else this . run ( $ ) . catch ( handler . bind ( $ ) ) ;
2020-12-15 01:44:28 +00:00
}
2021-04-03 09:58:20 +00:00
// Will return null if it successfully executes, SingleMessageOptions if there's an error (to let the user know what it is).
public async actualExecute ( args : string [ ] , tmp : any ) : Promise < SingleMessageOptions | null > {
// Subcommand Recursion //
let command = commands . get ( header ) ! ;
//resolveSubcommand()
const params : any [ ] = [ ] ;
let isEndpoint = false ;
let permLevel = command . permission ? ? 0 ;
for ( const param of args ) {
if ( command . endpoint ) {
if ( command . subcommands . size > 0 || command . user || command . number || command . any )
console . warn ( "An endpoint cannot have subcommands!" ) ;
isEndpoint = true ;
break ;
}
const type = command . resolve ( param ) ;
command = command . get ( param ) ;
permLevel = command . permission ? ? permLevel ;
if ( type === TYPES . USER ) {
const id = param . match ( /\d+/g ) ! [ 0 ] ;
try {
params . push ( await message . client . users . fetch ( id ) ) ;
} catch ( error ) {
return message . channel . send ( ` No user found by the ID \` ${ id } \` ! ` ) ;
}
} else if ( type === TYPES . NUMBER ) params . push ( Number ( param ) ) ;
else if ( type !== TYPES . SUBCOMMAND ) params . push ( param ) ;
}
if ( ! message . member )
return console . warn ( "This command was likely called from a DM channel meaning the member object is null." ) ;
if ( ! hasPermission ( message . member , permLevel ) ) {
const userPermLevel = getPermissionLevel ( message . member ) ;
return message . channel . send (
` You don't have access to this command! Your permission level is \` ${ getPermissionName (
userPermLevel
) } \ ` ( ${ userPermLevel } ), but this command requires a permission level of \` ${ getPermissionName (
permLevel
) } \ ` ( ${ permLevel } ). `
) ;
}
if ( isEndpoint ) return message . channel . send ( "Too many arguments!" ) ;
command . execute ( {
args : params ,
author : message.author ,
channel : message.channel ,
client : message.client ,
guild : message.guild ,
member : message.member ,
message : message
} ) ;
return null ;
}
2020-12-15 01:44:28 +00:00
public resolve ( param : string ) : TYPES {
if ( this . subcommands . has ( param ) ) return TYPES . SUBCOMMAND ;
// Any Discord ID format will automatically format to a user ID.
else if ( this . user && /\d{17,19}/ . test ( param ) ) return TYPES . USER ;
// Disallow infinity and allow for 0.
2020-12-15 07:56:09 +00:00
else if ( this . number && ( Number ( param ) || param === "0" ) && ! param . includes ( "Infinity" ) ) return TYPES . NUMBER ;
2020-12-15 01:44:28 +00:00
else if ( this . any ) return TYPES . ANY ;
else return TYPES . NONE ;
2020-10-15 09:23:24 +00:00
}
2020-12-15 01:44:28 +00:00
public get ( param : string ) : Command {
const type = this . resolve ( param ) ;
let command : Command ;
switch ( type ) {
case TYPES . SUBCOMMAND :
command = this . subcommands . get ( param ) as Command ;
break ;
case TYPES . USER :
command = this . user as Command ;
break ;
case TYPES . NUMBER :
command = this . number as Command ;
break ;
case TYPES . ANY :
command = this . any as Command ;
break ;
default :
command = this ;
break ;
}
return command ;
}
2020-07-25 08:15:26 +00:00
2021-04-03 09:58:20 +00:00
// Returns: [category, command name, command, available subcommands: [type, subcommand]]
public resolveCommandInfo ( args : string [ ] ) : [ string , string , Command , Collection < string , Command > ] {
const commands = await loadableCommands ;
let header = args . shift ( ) ;
let command = commands . get ( header ) ;
2020-12-15 01:44:28 +00:00
2021-04-03 09:58:20 +00:00
if ( ! command || header === "test" ) {
$ . channel . send ( ` No command found by the name \` ${ header } \` ! ` ) ;
return ;
}
2020-12-15 01:44:28 +00:00
2021-04-03 09:58:20 +00:00
if ( command . originalCommandName ) header = command . originalCommandName ;
else console . warn ( ` originalCommandName isn't defined for ${ header } ?! ` ) ;
2020-12-15 01:44:28 +00:00
2021-04-03 09:58:20 +00:00
let permLevel = command . permission ? ? 0 ;
let usage = command . usage ;
let invalid = false ;
let selectedCategory = "Unknown" ;
for ( const [ category , headers ] of categories ) {
if ( headers . includes ( header ) ) {
if ( selectedCategory !== "Unknown" )
console . warn (
` Command " ${ header } " is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories. `
) ;
else selectedCategory = toTitleCase ( category ) ;
2020-12-15 01:44:28 +00:00
}
2021-01-26 09:52:39 +00:00
}
2020-10-15 09:23:24 +00:00
2021-04-03 09:58:20 +00:00
for ( const param of args ) {
const type = command . resolve ( param ) ;
command = command . get ( param ) ;
permLevel = command . permission ? ? permLevel ;
2020-10-15 09:23:24 +00:00
2021-04-03 09:58:20 +00:00
if ( permLevel === - 1 ) permLevel = command . permission ;
2021-01-26 09:52:39 +00:00
2021-04-03 09:58:20 +00:00
switch ( type ) {
case TYPES . SUBCOMMAND :
header += ` ${ command . originalCommandName } ` ;
break ;
case TYPES . USER :
header += " <user>" ;
break ;
case TYPES . NUMBER :
header += " <number>" ;
break ;
case TYPES . ANY :
header += " <any>" ;
break ;
default :
header += ` ${ param } ` ;
break ;
2021-01-26 09:52:39 +00:00
}
2021-04-03 09:58:20 +00:00
if ( type === TYPES . NONE ) {
invalid = true ;
break ;
}
}
if ( invalid ) {
$ . channel . send ( ` No command found by the name \` ${ header } \` ! ` ) ;
return ;
}
}
2020-07-26 01:14:11 +00:00
}
2021-03-30 10:25:07 +00:00
// If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch(handler.bind($)); --> Manually caught by the user.
// TODO: Find a way to catch unhandled rejections automatically, forgoing the need for this.
export function handler ( this : CommandMenu , error : Error ) {
if ( this )
this . channel . send (
` There was an error while trying to execute that command! \` \` \` ${ error . stack ? ? error } \` \` \` `
) ;
else
console . warn (
"No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!"
) ;
console . error ( error ) ;
}