2020-12-15 01:44:28 +00:00
import $ , { isType , parseVars , CommonLibrary } from "./lib" ;
import { Collection } from "discord.js" ;
import { PERMISSIONS } from "./permissions" ;
import { getPrefix } from "../core/structures" ;
2021-01-26 09:52:39 +00:00
import glob from "glob" ;
2020-10-15 09:23:24 +00:00
interface CommandOptions {
2020-12-15 01:44:28 +00:00
description? : string ;
endpoint? : boolean ;
usage? : string ;
permission? : PERMISSIONS | null ;
aliases? : string [ ] ;
run ? : ( ( $ : CommonLibrary ) = > Promise < any > ) | string ;
subcommands ? : { [ key : string ] : Command } ;
user? : Command ;
number ? : Command ;
any ? : Command ;
2020-07-25 08:15:26 +00:00
}
2020-10-15 09:23:24 +00:00
export 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 ;
public readonly permission : PERMISSIONS | null ;
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?
public run : ( ( $ : CommonLibrary ) = > Promise < any > ) | string ;
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 ;
public static readonly TYPES = TYPES ;
public static readonly PERMISSIONS = PERMISSIONS ;
constructor ( options? : CommandOptions ) {
this . description = options ? . description || "No description." ;
this . endpoint = options ? . endpoint || false ;
this . usage = options ? . usage || "" ;
this . permission = options ? . permission ? ? null ;
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 ) )
$ . warn (
` " ${ 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 ) )
$ . warn (
` 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 )
2020-10-15 09:23:24 +00:00
$ . 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 )
2020-10-15 09:23:24 +00:00
$ . 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 )
$ . warn (
` 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
) ;
}
2020-12-15 01:44:28 +00:00
public execute ( $ : CommonLibrary ) {
if ( isType ( this . run , String ) ) {
$ . channel . send (
parseVars (
this . run as string ,
{
author : $.author.toString ( ) ,
prefix : getPrefix ( $ . guild )
} ,
"???"
)
) ;
} else ( this . run as Function ) ( $ ) . catch ( $ . handler . bind ( $ ) ) ;
}
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-01-26 09:52:39 +00:00
// Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command.
export const categories = new Collection < string , string [ ] > ( ) ;
2020-07-25 23:32:49 +00:00
/** Returns the cache of the commands if it exists and searches the directory if not. */
2021-01-26 09:52:39 +00:00
export const loadableCommands = ( async ( ) = > {
const commands = new Collection < string , Command > ( ) ;
// Include all .ts files recursively in "src/commands/".
const files = await globP ( "src/commands/**/*.ts" ) ;
// Extract the usable parts from "src/commands/" if:
// - The path is 1 to 2 subdirectories (a or a/b, not a/b/c)
// - Any leading directory isn't "modules"
// - The filename doesn't end in .test.ts (for jest testing)
// - The filename cannot be the hardcoded top-level "template.ts", reserved for generating templates
const pattern = /src\/commands\/(?!template\.ts)(?!modules\/)(\w+(?:\/\w+)?)(?:test\.)?\.ts/ ;
const lists : { [ category : string ] : string [ ] } = { } ;
for ( const path of files ) {
const match = pattern . exec ( path ) ;
if ( match ) {
const commandID = match [ 1 ] ; // e.g. "utilities/info"
const slashIndex = commandID . indexOf ( "/" ) ;
const isMiscCommand = slashIndex !== - 1 ;
const category = isMiscCommand ? commandID . substring ( 0 , slashIndex ) : "miscellaneous" ;
const commandName = isMiscCommand ? commandID . substring ( slashIndex + 1 ) : commandID ; // e.g. "info"
// If the dynamic import works, it must be an object at the very least. Then, just test to see if it's a proper instance.
const command = ( await import ( ` ../commands/ ${ commandID } ` ) ) . default as unknown ;
if ( command instanceof Command ) {
command . originalCommandName = commandName ;
if ( commands . has ( commandName ) ) {
$ . warn (
` Command " ${ commandName } " already exists! Make sure to make each command uniquely identifiable across categories! `
) ;
} else {
commands . set ( commandName , command ) ;
}
2020-12-15 01:44:28 +00:00
2021-01-26 09:52:39 +00:00
for ( const alias of command . aliases ) {
if ( commands . has ( alias ) ) {
$ . warn (
` Top-level alias " ${ alias } " from command " ${ commandID } " already exists either as a command or alias! `
) ;
} else {
commands . set ( alias , command ) ;
}
}
2020-12-15 01:44:28 +00:00
2021-01-26 09:52:39 +00:00
if ( ! ( category in lists ) ) lists [ category ] = [ ] ;
lists [ category ] . push ( commandName ) ;
2020-12-15 01:44:28 +00:00
2021-01-26 09:52:39 +00:00
$ . log ( ` Loading Command: ${ commandID } ` ) ;
} else {
$ . warn ( ` Command " ${ commandID } " has no default export which is a Command instance! ` ) ;
2020-12-15 01:44:28 +00:00
}
2021-01-26 09:52:39 +00:00
}
2020-12-15 01:44:28 +00:00
}
2020-10-15 09:23:24 +00:00
2021-01-26 09:52:39 +00:00
for ( const category in lists ) {
categories . set ( category , lists [ category ] ) ;
2020-12-15 01:44:28 +00:00
}
2020-10-15 09:23:24 +00:00
2021-01-26 09:52:39 +00:00
return commands ;
} ) ( ) ;
function globP ( path : string ) {
return new Promise < string [ ] > ( ( resolve , reject ) = > {
glob ( path , ( error , files ) = > {
if ( error ) {
reject ( error ) ;
} else {
resolve ( files ) ;
}
} ) ;
} ) ;
2020-07-26 01:14:11 +00:00
}