Reorganized code dealing with the command class
This commit is contained in:
parent
9adc5eea6e
commit
f650faee89
|
@ -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] = [];
|
|
||||||
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) {
|
for (const param of args) {
|
||||||
categories.set(category, lists[category]);
|
const type = command.resolve(param);
|
||||||
}
|
command = command.get(param);
|
||||||
|
permLevel = command.permission ?? permLevel;
|
||||||
|
|
||||||
return commands;
|
if (permLevel === -1) permLevel = command.permission;
|
||||||
})();
|
|
||||||
|
|
||||||
function globP(path: string) {
|
switch (type) {
|
||||||
return new Promise<string[]>((resolve, reject) => {
|
case TYPES.SUBCOMMAND:
|
||||||
glob(path, (error, files) => {
|
header += ` ${command.originalCommandName}`;
|
||||||
if (error) {
|
break;
|
||||||
reject(error);
|
case TYPES.USER:
|
||||||
} else {
|
header += " <user>";
|
||||||
resolve(files);
|
break;
|
||||||
|
case TYPES.NUMBER:
|
||||||
|
header += " <number>";
|
||||||
|
break;
|
||||||
|
case TYPES.ANY:
|
||||||
|
header += " <any>";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
header += ` ${param}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
if (type === TYPES.NONE) {
|
||||||
|
invalid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalid) {
|
||||||
|
$.channel.send(`No command found by the name \`${header}\`!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,139 +19,57 @@ client.on("message", async (message) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = await loadableCommands;
|
// Continue if the bot has permission to send messages in this channel.
|
||||||
|
|
||||||
let prefix = getPrefix(message.guild);
|
|
||||||
const originalPrefix = prefix;
|
|
||||||
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.
|
|
||||||
if (clientUser) {
|
|
||||||
// If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other).
|
|
||||||
// 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 (
|
if (
|
||||||
message.channel.type === "text" &&
|
message.channel.type === "dm" ||
|
||||||
!message.channel.permissionsFor(message.client.user || "")?.has(Permissions.FLAGS.SEND_MESSAGES)
|
message.channel.permissionsFor(client.user!)!.has(Permissions.FLAGS.SEND_MESSAGES)
|
||||||
) {
|
) {
|
||||||
let status;
|
const text = message.content;
|
||||||
|
const prefix = getPrefix(message.guild);
|
||||||
|
|
||||||
if (message.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR))
|
// First, test if the message is just a ping to the bot.
|
||||||
status =
|
if (new RegExp(`^<@!?${client.user!.id}>$`).test(text)) {
|
||||||
"Because you're a server admin, you have the ability to change that channel's permissions to match if that's what you intended.";
|
message.channel.send(`${message.author}, my prefix on this guild is \`${prefix}\`.`);
|
||||||
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,
|
|
||||||
channel: message.channel,
|
|
||||||
client: message.client,
|
|
||||||
guild: message.guild,
|
|
||||||
member: message.member,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
// 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 type = command.resolve(param);
|
if (commands.has(header)) {
|
||||||
command = command.get(param);
|
const command = commands.get(header)!;
|
||||||
permLevel = command.permission ?? permLevel;
|
|
||||||
|
|
||||||
if (type === Command.TYPES.USER) {
|
// Send the arguments to the command to resolve and execute.
|
||||||
const id = param.match(/\d+/g)![0];
|
// TMP[MAKE SURE TO REPLACE WITH command.execute WHEN FINISHED]
|
||||||
try {
|
const result = await command.actualExecute(args, {
|
||||||
params.push(await message.client.users.fetch(id));
|
author: message.author,
|
||||||
} catch (error) {
|
channel: message.channel,
|
||||||
return message.channel.send(`No user found by the ID \`${id}\`!`);
|
client: message.client,
|
||||||
|
guild: message.guild,
|
||||||
|
member: message.member,
|
||||||
|
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 if (type === Command.TYPES.NUMBER) params.push(Number(param));
|
}
|
||||||
else if (type !== Command.TYPES.SUBCOMMAND) params.push(param);
|
} 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."
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
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`
|
||||||
|
|
|
@ -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…
Reference in New Issue