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);
|
||||
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"
|
||||
|
||||
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"
|
||||
// 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…
Reference in a new issue