mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Finally made the commands directory configurable
This commit is contained in:
		
							parent
							
								
									653cc6f8a6
								
							
						
					
					
						commit
						4c3437a177
					
				
					 5 changed files with 91 additions and 46 deletions
				
			
		|  | @ -1,6 +1,5 @@ | |||
| import {Client, Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; | ||||
| import {loadableCommands} from "./loader"; | ||||
| import {getPrefix} from "./interface"; | ||||
| import {getPrefix, loadableCommands} from "./interface"; | ||||
| 
 | ||||
| // For custom message events that want to cancel the command handler on certain conditions.
 | ||||
| const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot]; | ||||
|  |  | |||
|  | @ -1,23 +1,52 @@ | |||
| import {Client, User, GuildMember, Guild} from "discord.js"; | ||||
| import {Collection, Client, User, GuildMember, Guild} from "discord.js"; | ||||
| import {attachMessageHandlerToClient} from "./handler"; | ||||
| import {attachEventListenersToClient} from "./eventListeners"; | ||||
| 
 | ||||
| interface LaunchSettings { | ||||
|     permissionLevels: PermissionLevel[]; | ||||
|     getPrefix: (guild: Guild | null) => string; | ||||
| } | ||||
| 
 | ||||
| export async function launch(client: Client, settings: LaunchSettings) { | ||||
|     attachMessageHandlerToClient(client); | ||||
|     attachEventListenersToClient(client); | ||||
|     permissionLevels = settings.permissionLevels; | ||||
|     getPrefix = settings.getPrefix; | ||||
| } | ||||
| import {NamedCommand} from "./command"; | ||||
| import {loadCommands} from "./loader"; | ||||
| 
 | ||||
| interface PermissionLevel { | ||||
|     name: string; | ||||
|     check: (user: User, member: GuildMember | null) => boolean; | ||||
| } | ||||
| 
 | ||||
| export let permissionLevels: PermissionLevel[] = []; | ||||
| export let getPrefix: (guild: Guild | null) => string = () => "."; | ||||
| type PrefixResolver = (guild: Guild | null) => string; | ||||
| type CategoryTransformer = (text: string) => string; | ||||
| 
 | ||||
| // One potential option is to let the user customize system messages such as "This command must be executed in a guild."
 | ||||
| // I decided not to do that because I don't think it'll be worth the trouble.
 | ||||
| interface LaunchSettings { | ||||
|     permissionLevels?: PermissionLevel[]; | ||||
|     getPrefix?: PrefixResolver; | ||||
|     categoryTransformer?: CategoryTransformer; | ||||
| } | ||||
| 
 | ||||
| // One alternative to putting everything in launch(client, ...) is to create an object then set each individual aspect, such as OnionCore.setPermissions(...).
 | ||||
| // That way, you can split different pieces of logic into different files, then do OnionCore.launch(client).
 | ||||
| // Additionally, each method would return the object so multiple methods could be chained, such as OnionCore.setPermissions(...).setPrefixResolver(...).launch(client).
 | ||||
| // I decided to not do this because creating a class then having a bunch of boilerplate around it just wouldn't really be worth it.
 | ||||
| // commandsDirectory requires an absolute path to work, so use __dirname.
 | ||||
| export async function launch(client: Client, commandsDirectory: string, settings?: LaunchSettings) { | ||||
|     // Core Launch Parameters //
 | ||||
|     loadableCommands = loadCommands(commandsDirectory); | ||||
|     attachMessageHandlerToClient(client); | ||||
|     attachEventListenersToClient(client); | ||||
| 
 | ||||
|     // Additional Configuration //
 | ||||
|     if (settings?.permissionLevels) { | ||||
|         if (settings.permissionLevels.length > 0) permissionLevels = settings.permissionLevels; | ||||
|         else console.warn("permissionLevels must have at least one element to work!"); | ||||
|     } | ||||
|     if (settings?.getPrefix) getPrefix = settings.getPrefix; | ||||
|     if (settings?.categoryTransformer) categoryTransformer = settings.categoryTransformer; | ||||
| } | ||||
| 
 | ||||
| // Placeholder until properly loaded by the user.
 | ||||
| export let loadableCommands = (async () => new Collection<string, NamedCommand>())(); | ||||
| export let permissionLevels: PermissionLevel[] = [ | ||||
|     { | ||||
|         name: "User", | ||||
|         check: () => true | ||||
|     } | ||||
| ]; | ||||
| export let getPrefix: PrefixResolver = () => "."; | ||||
| export let categoryTransformer: CategoryTransformer = (text) => text; | ||||
|  |  | |||
|  | @ -1,35 +1,46 @@ | |||
| import {Collection} from "discord.js"; | ||||
| import glob from "glob"; | ||||
| import path from "path"; | ||||
| import {NamedCommand, CommandInfo} from "./command"; | ||||
| import {toTitleCase} from "../lib"; | ||||
| import {loadableCommands, categoryTransformer} from "./interface"; | ||||
| 
 | ||||
| // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command.
 | ||||
| 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 () => { | ||||
| // This will go through all the .js files and import them. Because the import has to be .js (and cannot be .ts), there's no need for a custom filename checker in the launch settings.
 | ||||
| // This will avoid the problems of being a node module by requiring absolute imports, which the user will pass in as a launch parameter.
 | ||||
| export async function loadCommands(commandsDir: string): Promise<Collection<string, NamedCommand>> { | ||||
|     // Add a trailing separator so that the reduced filename list will reliably cut off the starting part.
 | ||||
|     // "C:/some/path/to/commands" --> "C:/some/path/to/commands/" (and likewise for \)
 | ||||
|     commandsDir = path.normalize(commandsDir); | ||||
|     if (!commandsDir.endsWith(path.sep)) commandsDir += path.sep; | ||||
| 
 | ||||
|     const commands = new Collection<string, NamedCommand>(); | ||||
|     // Include all .ts files recursively in "src/commands/".
 | ||||
|     const files = await globP("src/commands/**/*.ts"); | ||||
|     // Extract the usable parts from "src/commands/" if:
 | ||||
|     const files = await globP(path.join(commandsDir, "**", "*.js")); // This stage filters out source maps (.js.map).
 | ||||
|     // Because glob will use / regardless of platform, the following regex pattern can rely on / being the case.
 | ||||
|     const filesClean = files.map((filename) => filename.substring(commandsDir.length)); | ||||
|     // Extract the usable parts from commands directory 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/; | ||||
|     // - The filename doesn't end in .test.js (for jest testing)
 | ||||
|     // - The filename cannot be the hardcoded top-level "template.js", reserved for generating templates
 | ||||
|     const pattern = /^(?!template\.js)(?!modules\/)(\w+(?:\/\w+)?)(?:test\.)?\.js$/; | ||||
|     const lists: {[category: string]: string[]} = {}; | ||||
| 
 | ||||
|     for (const path of files) { | ||||
|         const match = pattern.exec(path); | ||||
| 
 | ||||
|         if (match) { | ||||
|     for (let i = 0; i < files.length; i++) { | ||||
|         const match = pattern.exec(filesClean[i]); | ||||
|         if (!match) continue; | ||||
|         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"
 | ||||
| 
 | ||||
|         // This try-catch block MUST be here or Node.js' dynamic require() will silently fail.
 | ||||
|         try { | ||||
|             // 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; | ||||
|             const command = (await import(files[i])).default as unknown; | ||||
| 
 | ||||
|             if (command instanceof NamedCommand) { | ||||
|                 command.name = commandName; | ||||
|  | @ -55,10 +66,12 @@ export const loadableCommands = (async () => { | |||
|                 if (!(category in lists)) lists[category] = []; | ||||
|                 lists[category].push(commandName); | ||||
| 
 | ||||
|                 console.log(`Loading Command: ${commandID}`); | ||||
|                 console.log(`Loaded Command: ${commandID}`); | ||||
|             } else { | ||||
|                 console.warn(`Command "${commandID}" has no default export which is a NamedCommand instance!`); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.log(error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -67,7 +80,7 @@ export const loadableCommands = (async () => { | |||
|     } | ||||
| 
 | ||||
|     return commands; | ||||
| })(); | ||||
| } | ||||
| 
 | ||||
| function globP(path: string) { | ||||
|     return new Promise<string[]>((resolve, reject) => { | ||||
|  | @ -92,7 +105,7 @@ export async function getCommandList(): Promise<Collection<string, NamedCommand[ | |||
|         const commandList: NamedCommand[] = []; | ||||
|         for (const header of headers.filter((header) => header !== "test")) commandList.push(commands.get(header)!); | ||||
|         // Ignore empty categories like "miscellaneous" (if it's empty).
 | ||||
|         if (commandList.length > 0) list.set(toTitleCase(category), commandList); | ||||
|         if (commandList.length > 0) list.set(categoryTransformer(category), commandList); | ||||
|     } | ||||
| 
 | ||||
|     return list; | ||||
|  | @ -120,7 +133,7 @@ export async function getCommandInfo(args: string[]): Promise<[CommandInfo, stri | |||
|     let category = "Unknown"; | ||||
|     for (const [referenceCategory, headers] of categories) { | ||||
|         if (headers.includes(header)) { | ||||
|             category = toTitleCase(referenceCategory); | ||||
|             category = categoryTransformer(referenceCategory); | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import {permissionLevels} from "./interface"; | |||
|  * Checks if a `Member` has a certain permission. | ||||
|  */ | ||||
| export function hasPermission(user: User, member: GuildMember | null, permission: number): boolean { | ||||
|     if (permissionLevels.length === 0) return true; | ||||
|     for (let i = permissionLevels.length - 1; i >= permission; i--) | ||||
|         if (permissionLevels[i].check(user, member)) return true; | ||||
|     return false; | ||||
|  | @ -20,6 +21,6 @@ export function getPermissionLevel(user: User, member: GuildMember | null): numb | |||
| } | ||||
| 
 | ||||
| export function getPermissionName(level: number) { | ||||
|     if (level > permissionLevels.length || level < 0) return "N/A"; | ||||
|     if (level > permissionLevels.length || level < 0 || permissionLevels.length === 0) return "N/A"; | ||||
|     else return permissionLevels[level].name; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										17
									
								
								src/index.ts
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								src/index.ts
									
										
									
									
									
								
							|  | @ -1,21 +1,25 @@ | |||
| // Bootstrapping Section //
 | ||||
| import "./modules/globals"; | ||||
| import {Client, Permissions} from "discord.js"; | ||||
| import {launch} from "./core"; | ||||
| import setup from "./modules/setup"; | ||||
| import {Config, getPrefix} from "./structures"; | ||||
| import path from "path"; | ||||
| 
 | ||||
| // This is here in order to make it much less of a headache to access the client from other files.
 | ||||
| // This of course won't actually do anything until the setup process is complete and it logs in.
 | ||||
| export const client = new Client(); | ||||
| 
 | ||||
| import {launch} from "./core"; | ||||
| import setup from "./modules/setup"; | ||||
| import {Config, getPrefix} from "./structures"; | ||||
| import {toTitleCase} from "./lib"; | ||||
| 
 | ||||
| // Send the login request to Discord's API and then load modules while waiting for it.
 | ||||
| setup.init().then(() => { | ||||
|     client.login(Config.token).catch(setup.again); | ||||
| }); | ||||
| 
 | ||||
| // Setup the command handler.
 | ||||
| launch(client, { | ||||
| launch(client, path.join(__dirname, "commands"), { | ||||
|     getPrefix, | ||||
|     categoryTransformer: toTitleCase, | ||||
|     permissionLevels: [ | ||||
|         { | ||||
|             // NONE //
 | ||||
|  | @ -57,8 +61,7 @@ launch(client, { | |||
|             name: "Bot Owner", | ||||
|             check: (user) => Config.owner === user.id | ||||
|         } | ||||
|     ], | ||||
|     getPrefix: getPrefix | ||||
|     ] | ||||
| }); | ||||
| 
 | ||||
| // Initialize Modules //
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue