From 0d0d1344156d9f4c9fecfbdf59ce97894d60a8f6 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Wed, 31 Mar 2021 03:41:43 -0500 Subject: [PATCH] Tried experimental porting of command types from CrossExchange --- src/commands/system/help.ts | 12 +-- src/core/command.ts | 148 ++++++++++++++++++++++++++---------- src/core/handler.ts | 72 +++++++++++++++--- 3 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 8ef9fe2..cfe243b 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -1,4 +1,4 @@ -import Command from "../../core/command"; +import Command, {TYPES} from "../../core/command"; import {toTitleCase} from "../../core/lib"; import {loadableCommands, categories} from "../../core/command"; import {getPermissionName} from "../../core/permissions"; @@ -68,16 +68,16 @@ export default new Command({ if (permLevel === -1) permLevel = command.permission; switch (type) { - case Command.TYPES.SUBCOMMAND: + case TYPES.SUBCOMMAND: header += ` ${command.originalCommandName}`; break; - case Command.TYPES.USER: + case TYPES.USER: header += " "; break; - case Command.TYPES.NUMBER: + case TYPES.NUMBER: header += " "; break; - case Command.TYPES.ANY: + case TYPES.ANY: header += " "; break; default: @@ -85,7 +85,7 @@ export default new Command({ break; } - if (type === Command.TYPES.NONE) { + if (type === TYPES.NONE) { invalid = true; break; } diff --git a/src/core/command.ts b/src/core/command.ts index c13b3a3..fdac3a6 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -1,6 +1,5 @@ import {parseVars} from "./lib"; -import {Collection} from "discord.js"; -import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js"; +import {Collection, Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js"; import {getPrefix} from "../core/structures"; import glob from "glob"; @@ -22,17 +21,27 @@ interface CommandOptions { aliases?: string[]; run?: (($: CommandMenu) => Promise) | string; subcommands?: {[key: string]: Command}; + channel?: Command; + role?: Command; + emote?: Command; + message?: Command; user?: Command; + id?: "channel" | "role" | "emote" | "message" | "user"; number?: Command; any?: Command; } export enum TYPES { - SUBCOMMAND, - USER, - NUMBER, - ANY, - NONE + SUBCOMMAND, // Any specifically-defined keywords / string literals. + CHANNEL, // <#...> + ROLE, // <@&...> + EMOTE, // <::ID> (The previous two values, animated and emote name respectively, do not matter at all for finding the emote.) + MESSAGE, // Available by using the built-in "Copy Message Link" or "Copy ID" buttons. https://discordapp.com/channels/// or - (automatically searches all guilds for the channel ID). + USER, // <@...> and <@!...> + ID, // Any number with 17-19 digits. Only used as a redirect to another subcommand type. + NUMBER, // Any valid number via the Number() function, except for NaN and Infinity (because those can really mess with the program). + ANY, // Generic argument case. + NONE // No subcommands exist. } export default class Command { @@ -44,10 +53,14 @@ export default class Command { public originalCommandName: string | null; // If the command is an alias, what's the original name? public run: (($: CommandMenu) => Promise) | string; public readonly subcommands: Collection; // This is the final data structure you'll actually use to work with the commands the aliases point to. + public channel: Command | null; + public role: Command | null; + public emote: Command | null; + public message: Command | null; public user: Command | null; + public id: Command | null; public number: Command | null; public any: Command | null; - public static readonly TYPES = TYPES; constructor(options?: CommandOptions) { this.description = options?.description || "No description."; @@ -58,10 +71,35 @@ export default class Command { this.originalCommandName = null; this.run = options?.run || "No action was set on this command!"; this.subcommands = new Collection(); // Populate this collection after setting subcommands. + this.channel = options?.channel || null; + this.role = options?.role || null; + this.emote = options?.emote || null; + this.message = options?.message || null; this.user = options?.user || null; this.number = options?.number || null; this.any = options?.any || null; + switch (options?.id) { + case "channel": + this.id = this.channel; + break; + case "role": + this.id = this.role; + break; + case "emote": + this.id = this.emote; + break; + case "message": + this.id = this.message; + break; + case "user": + this.id = this.user; + break; + default: + this.id = null; + break; + } + if (options?.subcommands) { const baseSubcommands = Object.keys(options.subcommands); @@ -89,20 +127,28 @@ export default class Command { } } - if (this.user && this.user.aliases.length > 0) - console.warn( - `There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` - ); + // Because command aliases don't actually do anything except for subcommands, let the user know that this won't do anything. + warnCommandAliases(this.channel, "channel"); + warnCommandAliases(this.role, "role"); + warnCommandAliases(this.emote, "emote"); + warnCommandAliases(this.message, "message"); + warnCommandAliases(this.user, "user"); + warnCommandAliases(this.number, "number"); + warnCommandAliases(this.any, "any"); - if (this.number && this.number.aliases.length > 0) - console.warn( - `There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` - ); - - if (this.any && this.any.aliases.length > 0) - console.warn( - `There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` - ); + // Warn on unused endpoints too. + if ( + this.endpoint && + (this.subcommands.size > 0 || + this.channel || + this.role || + this.emote || + this.message || + this.user || + this.number || + this.any) + ) + console.warn(`An endpoint cannot have subcommands!`); } public execute($: CommandMenu) { @@ -121,41 +167,61 @@ export default class Command { } public resolve(param: string): TYPES { + if (this.id && /^\d{17,19}$/.test(param)) { + } + if (this.subcommands.has(param)) return TYPES.SUBCOMMAND; - // Any Discord ID format will automatically format to a user ID. - else if (this.user && /\d{17,19}/.test(param)) return TYPES.USER; + else if (this.channel && /^<#\d{17,19}>$/.test(param)) return TYPES.CHANNEL; + else if (this.role && /^<@&\d{17,19}>$/.test(param)) return TYPES.ROLE; + else if (this.emote && /^$/.test(param)) return TYPES.EMOTE; + else if (this.message && /(\d{17,19}\/\d{17,19}\/\d{17,19}$)|(^\d{17,19}-\d{17,19}$)/.test(param)) + return TYPES.MESSAGE; + else if (this.user && /^<@!?\d{17,19}>$/.test(param)) return TYPES.USER; // Disallow infinity and allow for 0. - else if (this.number && (Number(param) || param === "0") && !param.includes("Infinity")) return TYPES.NUMBER; + else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") + return TYPES.NUMBER; else if (this.any) return TYPES.ANY; else return TYPES.NONE; } - public get(param: string): Command { - const type = this.resolve(param); - let command: Command; - - switch (type) { + // You can also optionally send in a pre-calculated value if you already called Command.resolve so you don't call it again. + public get(param: string, type?: TYPES): Command { + // This expression only runs once, don't worry. + switch (type ?? this.resolve(param)) { case TYPES.SUBCOMMAND: - command = this.subcommands.get(param) as Command; - break; + return checkResolvedCommand(this.subcommands.get(param)); + case TYPES.CHANNEL: + return checkResolvedCommand(this.channel); + case TYPES.ROLE: + return checkResolvedCommand(this.role); + case TYPES.EMOTE: + return checkResolvedCommand(this.emote); + case TYPES.MESSAGE: + return checkResolvedCommand(this.message); case TYPES.USER: - command = this.user as Command; - break; + return checkResolvedCommand(this.user); case TYPES.NUMBER: - command = this.number as Command; - break; + return checkResolvedCommand(this.number); case TYPES.ANY: - command = this.any as Command; - break; + return checkResolvedCommand(this.any); default: - command = this; - break; + return this; } - - return command; } } +function warnCommandAliases(command: Command | null, type: string) { + if (command && command.aliases.length > 0) + console.warn( + `There are aliases defined for an "${type}"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` + ); +} + +function checkResolvedCommand(command: Command | null | undefined): Command { + if (!command) throw new Error("FATAL: Command type mismatch while calling Command.get!"); + 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. export const categories = new Collection(); diff --git a/src/core/handler.ts b/src/core/handler.ts index 1de6c1e..217437f 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -1,5 +1,5 @@ import {client} from "../index"; -import Command, {loadableCommands} from "../core/command"; +import Command, {loadableCommands, TYPES} from "../core/command"; import {hasPermission, getPermissionLevel, getPermissionName} from "../core/permissions"; import {Permissions} from "discord.js"; import {getPrefix} from "../core/structures"; @@ -102,18 +102,70 @@ client.on("message", async (message) => { } const type = command.resolve(param); - command = command.get(param); + command = command.get(param, type); 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}\`!`); + // Add argument to parameter list, do different stuff depending on the subcommand type. + switch (type) { + case TYPES.SUBCOMMAND: + break; + case TYPES.CHANNEL: { + const id = param.match(/^<#(\d{17,19})>$/)![1]; + console.debug(id); + try { + params.push(message.guild!.channels.cache.get(id)); + } catch (error) { + return message.channel.send(`No channel found by the ID \`${id}\` in this guild!`); + } + break; } - } else if (type === Command.TYPES.NUMBER) params.push(Number(param)); - else if (type !== Command.TYPES.SUBCOMMAND) params.push(param); + case TYPES.ROLE: { + const id = param.match(/^<@&(\d{17,19})>$/)![1]; + console.debug(id); + try { + params.push(await message.guild!.roles.fetch(id)); + } catch (error) { + return message.channel.send(`No role found by the ID \`${id}\` in this guild!`); + } + break; + } + case TYPES.EMOTE: { + const id = param.match(/^$/)![1]; + console.debug(id); + try { + params.push(message.client.emojis.cache.get(id)); + } catch (error) { + return message.channel.send(`No emote found by the ID \`${id}\`!`); + } + break; + } + case TYPES.MESSAGE: { + const id = param.match(/(\d{17,19}\/\d{17,19}\/\d{17,19}$)|(^\d{17,19}-\d{17,19}$)/); + console.log(id); + /*try { + params.push(await message.client.users.fetch(id)); + } catch (error) { + return message.channel.send(`No user found by the ID \`${id}\`!`); + }*/ + break; + } + case TYPES.USER: { + const id = param.match(/^<@!?(\d{17,19})>$/)![1]; + console.debug(param, id); + try { + params.push(await message.client.users.fetch(id)); + } catch (error) { + return message.channel.send(`No user found by the ID \`${id}\`!`); + } + break; + } + case TYPES.NUMBER: + params.push(Number(param)); + break; + case TYPES.ANY: + params.push(param); + break; + } } if (!message.member)