mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Reorganized code dealing with the command class
This commit is contained in:
		
							parent
							
								
									9adc5eea6e
								
							
						
					
					
						commit
						f650faee89
					
				
					 4 changed files with 269 additions and 279 deletions
				
			
		|  | @ -1,6 +1,6 @@ | ||||||
| import Command from "../../core/command"; | import Command from "../../core/command"; | ||||||
| import {toTitleCase} from "../../core/lib"; | import {toTitleCase} from "../../core/lib"; | ||||||
| import {loadableCommands, categories} from "../../core/command"; | import {loadableCommands, categories} from "../../core/loader"; | ||||||
| import {getPermissionName} from "../../core/permissions"; | import {getPermissionName} from "../../core/permissions"; | ||||||
| 
 | 
 | ||||||
| export default new Command({ | export default new Command({ | ||||||
|  | @ -32,69 +32,7 @@ export default new Command({ | ||||||
|     }, |     }, | ||||||
|     any: new Command({ |     any: new Command({ | ||||||
|         async run($) { |         async run($) { | ||||||
|             const commands = await loadableCommands; |             // [category, commandName, command, subcommandInfo] = resolveCommandInfo();
 | ||||||
|             let header = $.args.shift() as string; |  | ||||||
|             let command = commands.get(header); |  | ||||||
| 
 |  | ||||||
|             if (!command || header === "test") { |  | ||||||
|                 $.channel.send(`No command found by the name \`${header}\`!`); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (command.originalCommandName) header = command.originalCommandName; |  | ||||||
|             else console.warn(`originalCommandName isn't defined for ${header}?!`); |  | ||||||
| 
 |  | ||||||
|             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); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             for (const param of $.args) { |  | ||||||
|                 const type = command.resolve(param); |  | ||||||
|                 command = command.get(param); |  | ||||||
|                 permLevel = command.permission ?? permLevel; |  | ||||||
| 
 |  | ||||||
|                 if (permLevel === -1) permLevel = command.permission; |  | ||||||
| 
 |  | ||||||
|                 switch (type) { |  | ||||||
|                     case Command.TYPES.SUBCOMMAND: |  | ||||||
|                         header += ` ${command.originalCommandName}`; |  | ||||||
|                         break; |  | ||||||
|                     case Command.TYPES.USER: |  | ||||||
|                         header += " <user>"; |  | ||||||
|                         break; |  | ||||||
|                     case Command.TYPES.NUMBER: |  | ||||||
|                         header += " <number>"; |  | ||||||
|                         break; |  | ||||||
|                     case Command.TYPES.ANY: |  | ||||||
|                         header += " <any>"; |  | ||||||
|                         break; |  | ||||||
|                     default: |  | ||||||
|                         header += ` ${param}`; |  | ||||||
|                         break; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (type === Command.TYPES.NONE) { |  | ||||||
|                     invalid = true; |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             if (invalid) { |  | ||||||
|                 $.channel.send(`No command found by the name \`${header}\`!`); |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             let append = ""; |             let append = ""; | ||||||
| 
 | 
 | ||||||
|  | @ -123,18 +61,10 @@ export default new Command({ | ||||||
|                 append = "Usages:" + (list.length > 0 ? `\n${list.join("\n")}` : " None."); |                 append = "Usages:" + (list.length > 0 ? `\n${list.join("\n")}` : " None."); | ||||||
|             } else append = `Usage: \`${header} ${usage}\``; |             } else append = `Usage: \`${header} ${usage}\``; | ||||||
| 
 | 
 | ||||||
|             let aliases = "None"; |             const formattedAliases: string[] = []; | ||||||
| 
 |             for (const alias of command.aliases) formattedAliases.push(`\`${alias}\``); | ||||||
|             if (command.aliases.length > 0) { |             // Short circuit an empty string, in this case, if there are no aliases.
 | ||||||
|                 aliases = ""; |             const aliases = formattedAliases.join(", ") || "None"; | ||||||
| 
 |  | ||||||
|                 for (let i = 0; i < command.aliases.length; i++) { |  | ||||||
|                     const alias = command.aliases[i]; |  | ||||||
|                     aliases += `\`${alias}\``; |  | ||||||
| 
 |  | ||||||
|                     if (i !== command.aliases.length - 1) aliases += ", "; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             $.channel.send( |             $.channel.send( | ||||||
|                 `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${getPermissionName( |                 `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${getPermissionName( | ||||||
|  |  | ||||||
|  | @ -2,7 +2,8 @@ import {parseVars} from "./lib"; | ||||||
| import {Collection} from "discord.js"; | import {Collection} from "discord.js"; | ||||||
| import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js"; | import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js"; | ||||||
| import {getPrefix} from "../core/structures"; | import {getPrefix} from "../core/structures"; | ||||||
| import glob from "glob"; | import {SingleMessageOptions} from "./libd"; | ||||||
|  | import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; | ||||||
| 
 | 
 | ||||||
| interface CommandMenu { | interface CommandMenu { | ||||||
|     args: any[]; |     args: any[]; | ||||||
|  | @ -27,7 +28,7 @@ interface CommandOptions { | ||||||
|     any?: Command; |     any?: Command; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export enum TYPES { | enum TYPES { | ||||||
|     SUBCOMMAND, |     SUBCOMMAND, | ||||||
|     USER, |     USER, | ||||||
|     NUMBER, |     NUMBER, | ||||||
|  | @ -47,7 +48,6 @@ export default class Command { | ||||||
|     public user: Command | null; |     public user: Command | null; | ||||||
|     public number: Command | null; |     public number: Command | null; | ||||||
|     public any: Command | null; |     public any: Command | null; | ||||||
|     public static readonly TYPES = TYPES; |  | ||||||
| 
 | 
 | ||||||
|     constructor(options?: CommandOptions) { |     constructor(options?: CommandOptions) { | ||||||
|         this.description = options?.description || "No description."; |         this.description = options?.description || "No description."; | ||||||
|  | @ -120,6 +120,67 @@ export default class Command { | ||||||
|         } else this.run($).catch(handler.bind($)); |         } else this.run($).catch(handler.bind($)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // 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; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public resolve(param: string): TYPES { |     public resolve(param: string): TYPES { | ||||||
|         if (this.subcommands.has(param)) return TYPES.SUBCOMMAND; |         if (this.subcommands.has(param)) return TYPES.SUBCOMMAND; | ||||||
|         // Any Discord ID format will automatically format to a user ID.
 |         // Any Discord ID format will automatically format to a user ID.
 | ||||||
|  | @ -154,84 +215,73 @@ export default class Command { | ||||||
| 
 | 
 | ||||||
|         return command; |         return command; | ||||||
|     } |     } | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command.
 |     // Returns: [category, command name, command, available subcommands: [type, subcommand]]
 | ||||||
| export const categories = new Collection<string, string[]>(); |     public resolveCommandInfo(args: string[]): [string, string, Command, Collection<string, Command>] { | ||||||
|  |         const commands = await loadableCommands; | ||||||
|  |         let header = args.shift(); | ||||||
|  |         let command = commands.get(header); | ||||||
| 
 | 
 | ||||||
| /** Returns the cache of the commands if it exists and searches the directory if not. */ |         if (!command || header === "test") { | ||||||
| export const loadableCommands = (async () => { |             $.channel.send(`No command found by the name \`${header}\`!`); | ||||||
|     const commands = new Collection<string, Command>(); |             return; | ||||||
|     // 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) { |         if (command.originalCommandName) header = command.originalCommandName; | ||||||
|         const match = pattern.exec(path); |         else console.warn(`originalCommandName isn't defined for ${header}?!`); | ||||||
| 
 | 
 | ||||||
|         if (match) { |         let permLevel = command.permission ?? 0; | ||||||
|             const commandID = match[1]; // e.g. "utilities/info"
 |         let usage = command.usage; | ||||||
|             const slashIndex = commandID.indexOf("/"); |         let invalid = false; | ||||||
|             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) { |         let selectedCategory = "Unknown"; | ||||||
|                 command.originalCommandName = commandName; |  | ||||||
| 
 | 
 | ||||||
|                 if (commands.has(commandName)) { |         for (const [category, headers] of categories) { | ||||||
|  |             if (headers.includes(header)) { | ||||||
|  |                 if (selectedCategory !== "Unknown") | ||||||
|                     console.warn( |                     console.warn( | ||||||
|                         `Command "${commandName}" already exists! Make sure to make each command uniquely identifiable across categories!` |                         `Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.` | ||||||
|                     ); |                     ); | ||||||
|                 } else { |                 else selectedCategory = toTitleCase(category); | ||||||
|                     commands.set(commandName, command); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 for (const alias of command.aliases) { |  | ||||||
|                     if (commands.has(alias)) { |  | ||||||
|                         console.warn( |  | ||||||
|                             `Top-level alias "${alias}" from command "${commandID}" already exists either as a command or alias!` |  | ||||||
|                         ); |  | ||||||
|                     } else { |  | ||||||
|                         commands.set(alias, command); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|                 if (!(category in lists)) lists[category] = []; |         for (const param of args) { | ||||||
|                 lists[category].push(commandName); |             const type = command.resolve(param); | ||||||
|  |             command = command.get(param); | ||||||
|  |             permLevel = command.permission ?? permLevel; | ||||||
| 
 | 
 | ||||||
|                 console.log(`Loading Command: ${commandID}`); |             if (permLevel === -1) permLevel = command.permission; | ||||||
|             } else { | 
 | ||||||
|                 console.warn(`Command "${commandID}" has no default export which is a Command instance!`); |             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; | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             if (type === TYPES.NONE) { | ||||||
|  |                 invalid = true; | ||||||
|  |                 break; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     for (const category in lists) { |         if (invalid) { | ||||||
|         categories.set(category, lists[category]); |             $.channel.send(`No command found by the name \`${header}\`!`); | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     return commands; |  | ||||||
| })(); |  | ||||||
| 
 |  | ||||||
| function globP(path: string) { |  | ||||||
|     return new Promise<string[]>((resolve, reject) => { |  | ||||||
|         glob(path, (error, files) => { |  | ||||||
|             if (error) { |  | ||||||
|                 reject(error); |  | ||||||
|             } else { |  | ||||||
|                 resolve(files); |  | ||||||
|     } |     } | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // If you use promises, use this function to display the error in chat.
 | // If you use promises, use this function to display the error in chat.
 | ||||||
|  |  | ||||||
|  | @ -1,28 +1,17 @@ | ||||||
| import {client} from "../index"; | import {client} from "../index"; | ||||||
| import Command, {loadableCommands} from "./command"; | import {loadableCommands} from "./loader"; | ||||||
| import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; |  | ||||||
| import {Permissions, Message} from "discord.js"; | import {Permissions, Message} from "discord.js"; | ||||||
| import {getPrefix} from "./structures"; | import {getPrefix} from "./structures"; | ||||||
| import {Config} from "./structures"; | import {Config} from "./structures"; | ||||||
| 
 | 
 | ||||||
| ///////////
 | // For custom message events that want to cancel the command handler on certain conditions.
 | ||||||
| // Steps //
 |  | ||||||
| ///////////
 |  | ||||||
| // 1. Someone sends a message in chat.
 |  | ||||||
| // 2. Check if bot, then load commands.
 |  | ||||||
| // 3. Check if "<prefix>...". If not, check if "@<bot>...". Resolve prefix and cropped message (if possible).
 |  | ||||||
| // 4. Test if bot has permission to send messages.
 |  | ||||||
| // 5. Once confirmed as a command, resolve the subcommand.
 |  | ||||||
| // 6. Check permission level and whether or not it's an endpoint.
 |  | ||||||
| // 7. Execute command if all successful.
 |  | ||||||
| 
 |  | ||||||
| // For custom message events that want to cancel this one on certain conditions.
 |  | ||||||
| const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot]; | const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot]; | ||||||
| 
 | 
 | ||||||
| export function addInterceptRule(handler: (message: Message) => boolean) { | export function addInterceptRule(handler: (message: Message) => boolean) { | ||||||
|     interceptRules.push(handler); |     interceptRules.push(handler); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined.
 | ||||||
| client.on("message", async (message) => { | client.on("message", async (message) => { | ||||||
|     for (const shouldIntercept of interceptRules) { |     for (const shouldIntercept of interceptRules) { | ||||||
|         if (shouldIntercept(message)) { |         if (shouldIntercept(message)) { | ||||||
|  | @ -30,93 +19,29 @@ client.on("message", async (message) => { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Continue if the bot has permission to send messages in this channel.
 | ||||||
|  |     if ( | ||||||
|  |         message.channel.type === "dm" || | ||||||
|  |         message.channel.permissionsFor(client.user!)!.has(Permissions.FLAGS.SEND_MESSAGES) | ||||||
|  |     ) { | ||||||
|  |         const text = message.content; | ||||||
|  |         const prefix = getPrefix(message.guild); | ||||||
|  | 
 | ||||||
|  |         // First, test if the message is just a ping to the bot.
 | ||||||
|  |         if (new RegExp(`^<@!?${client.user!.id}>$`).test(text)) { | ||||||
|  |             message.channel.send(`${message.author}, my prefix on this guild is \`${prefix}\`.`); | ||||||
|  |         } | ||||||
|  |         // Then check if it's a normal command.
 | ||||||
|  |         else if (text.startsWith(prefix)) { | ||||||
|  |             const [header, ...args] = text.substring(prefix.length).split(/ +/); | ||||||
|             const commands = await loadableCommands; |             const commands = await loadableCommands; | ||||||
| 
 | 
 | ||||||
|     let prefix = getPrefix(message.guild); |             if (commands.has(header)) { | ||||||
|     const originalPrefix = prefix; |                 const command = commands.get(header)!; | ||||||
|     let exitEarly = !message.content.startsWith(prefix); |  | ||||||
|     const clientUser = message.client.user; |  | ||||||
|     let usesBotSpecificPrefix = false; |  | ||||||
| 
 | 
 | ||||||
|     // If the client user exists, check if it starts with the bot-specific prefix.
 |                 // Send the arguments to the command to resolve and execute.
 | ||||||
|     if (clientUser) { |                 // TMP[MAKE SURE TO REPLACE WITH command.execute WHEN FINISHED]
 | ||||||
|         // If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other).
 |                 const result = await command.actualExecute(args, { | ||||||
|         // The pattern here has an optional space at the end to capture that and make it not mess with the header and args.
 |  | ||||||
|         const matches = message.content.match(new RegExp(`^<@!?${clientUser.id}> ?`)); |  | ||||||
| 
 |  | ||||||
|         if (matches) { |  | ||||||
|             prefix = matches[0]; |  | ||||||
|             exitEarly = false; |  | ||||||
|             usesBotSpecificPrefix = true; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // If it doesn't start with the current normal prefix or the bot-specific unique prefix, exit the thread of execution early.
 |  | ||||||
|     // Inline replies should still be captured here because if it doesn't exit early, two characters for a two-length prefix would still trigger commands.
 |  | ||||||
|     if (exitEarly) return; |  | ||||||
| 
 |  | ||||||
|     const [header, ...args] = message.content.substring(prefix.length).split(/ +/); |  | ||||||
| 
 |  | ||||||
|     // If the message is just the prefix itself, move onto this block.
 |  | ||||||
|     if (header === "" && args.length === 0) { |  | ||||||
|         // I moved the bot-specific prefix to a separate conditional block to separate the logic.
 |  | ||||||
|         // And because it listens for the mention as a prefix instead of a free-form mention, inline replies (probably) shouldn't ever trigger this unintentionally.
 |  | ||||||
|         if (usesBotSpecificPrefix) { |  | ||||||
|             message.channel.send(`${message.author.toString()}, my prefix on this guild is \`${originalPrefix}\`.`); |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!commands.has(header)) return; |  | ||||||
| 
 |  | ||||||
|     if ( |  | ||||||
|         message.channel.type === "text" && |  | ||||||
|         !message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES) |  | ||||||
|     ) { |  | ||||||
|         let status; |  | ||||||
| 
 |  | ||||||
|         if (message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR)) |  | ||||||
|             status = |  | ||||||
|                 "Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended."; |  | ||||||
|         else |  | ||||||
|             status = |  | ||||||
|                 "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong."; |  | ||||||
| 
 |  | ||||||
|         return message.author.send( |  | ||||||
|             `I don't have permission to send messages in ${message.channel.toString()}. ${status}` |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log( |  | ||||||
|         `${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".` |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Subcommand Recursion //
 |  | ||||||
|     let command = commands.get(header)!; |  | ||||||
|     //resolveSubcommand()
 |  | ||||||
| 
 |  | ||||||
|     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!"); |  | ||||||
| 
 |  | ||||||
|     // Execute with dynamic library attached. //
 |  | ||||||
|     // The purpose of using $.bind($) is to clone the function so as to not modify the original $.
 |  | ||||||
|     // The cloned function doesn't copy the properties, so Object.assign() is used.
 |  | ||||||
|     // Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
 |  | ||||||
|     command.execute({ |  | ||||||
|         args: params, |  | ||||||
|                     author: message.author, |                     author: message.author, | ||||||
|                     channel: message.channel, |                     channel: message.channel, | ||||||
|                     client: message.client, |                     client: message.client, | ||||||
|  | @ -124,45 +49,27 @@ client.on("message", async (message) => { | ||||||
|                     member: message.member, |                     member: message.member, | ||||||
|                     message: message |                     message: message | ||||||
|                 }); |                 }); | ||||||
|  | 
 | ||||||
|  |                 // If something went wrong, let the user know (like if they don't have permission to use a command).
 | ||||||
|  |                 if (result) { | ||||||
|  |                     message.channel.send(result); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         message.author.send( | ||||||
|  |             `I don't have permission to send messages in ${message.channel}. ${ | ||||||
|  |                 message.member!.hasPermission(Permissions.FLAGS.ADMINISTRATOR) | ||||||
|  |                     ? "Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended." | ||||||
|  |                     : "Try using a different channel or contacting a server admin to change permissions of that channel if you think something's wrong." | ||||||
|  |             }` | ||||||
|  |         ); | ||||||
|  |     } | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // Takes a base command and a list of string parameters and returns:
 |  | ||||||
| // - The resolved subcommand
 |  | ||||||
| // - The resolved parameters
 |  | ||||||
| // - Whether or not an endpoint has been broken
 |  | ||||||
| // - The permission level required
 |  | ||||||
| async function resolveSubcommand(command: Command, args: string[]): [Command, any[], boolean, number] { |  | ||||||
|     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 === Command.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 === Command.TYPES.NUMBER) params.push(Number(param)); |  | ||||||
|         else if (type !== Command.TYPES.SUBCOMMAND) params.push(param); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| client.once("ready", () => { | client.once("ready", () => { | ||||||
|     if (client.user) { |     if (client.user) { | ||||||
|         console.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`); |         console.ready(`Logged in as ${client.user.tag}.`); | ||||||
|         client.user.setActivity({ |         client.user.setActivity({ | ||||||
|             type: "LISTENING", |             type: "LISTENING", | ||||||
|             name: `${Config.prefix}help` |             name: `${Config.prefix}help` | ||||||
|  |  | ||||||
							
								
								
									
										103
									
								
								src/core/loader.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/core/loader.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | ||||||
|  | import {Collection} from "discord.js"; | ||||||
|  | import glob from "glob"; | ||||||
|  | import Command from "./command"; | ||||||
|  | 
 | ||||||
|  | // 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[]>(); | ||||||
|  | 
 | ||||||
|  | /** Returns the cache of the commands if it exists and searches the directory if not. */ | ||||||
|  | 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)) { | ||||||
|  |                     console.warn( | ||||||
|  |                         `Command "${commandName}" already exists! Make sure to make each command uniquely identifiable across categories!` | ||||||
|  |                     ); | ||||||
|  |                 } else { | ||||||
|  |                     commands.set(commandName, command); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 for (const alias of command.aliases) { | ||||||
|  |                     if (commands.has(alias)) { | ||||||
|  |                         console.warn( | ||||||
|  |                             `Top-level alias "${alias}" from command "${commandID}" already exists either as a command or alias!` | ||||||
|  |                         ); | ||||||
|  |                     } else { | ||||||
|  |                         commands.set(alias, command); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (!(category in lists)) lists[category] = []; | ||||||
|  |                 lists[category].push(commandName); | ||||||
|  | 
 | ||||||
|  |                 console.log(`Loading Command: ${commandID}`); | ||||||
|  |             } else { | ||||||
|  |                 console.warn(`Command "${commandID}" has no default export which is a Command instance!`); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const category in lists) { | ||||||
|  |         categories.set(category, lists[category]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return commands; | ||||||
|  | })(); | ||||||
|  | 
 | ||||||
|  | function globP(path: string) { | ||||||
|  |     return new Promise<string[]>((resolve, reject) => { | ||||||
|  |         glob(path, (error, files) => { | ||||||
|  |             if (error) { | ||||||
|  |                 reject(error); | ||||||
|  |             } else { | ||||||
|  |                 resolve(files); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Gathers a list of categories and top-level commands.
 | ||||||
|  | // Returns: new Collection<string category, Command[] commandList>()
 | ||||||
|  | /*export async function getCommandList(): Promise<Collection<string, Command[]>> { | ||||||
|  |     const categorizedCommands = new Collection<string, Command[]>(); | ||||||
|  |     const commands = await loadableCommands; | ||||||
|  | 
 | ||||||
|  |     for (const [category, headers] of categories) { | ||||||
|  |         const commandList: Command[] = []; | ||||||
|  | 
 | ||||||
|  |         for (const header of headers) { | ||||||
|  |             if (header !== "test") { | ||||||
|  |                 // If this is somehow undefined, it'll show up as an error when implementing a help command.
 | ||||||
|  |                 commandList.push(commands.get(header)!); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         categorizedCommands.set(toTitleCase(category), commandList); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return categorizedCommands; | ||||||
|  | }*/ | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue