mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Added more subcommand types
This commit is contained in:
parent
03f37680e7
commit
4a78ce808b
2 changed files with 261 additions and 23 deletions
|
@ -13,7 +13,7 @@ import {
|
||||||
import {SingleMessageOptions} from "./libd";
|
import {SingleMessageOptions} from "./libd";
|
||||||
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
|
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
|
||||||
import {getPrefix} from "./interface";
|
import {getPrefix} from "./interface";
|
||||||
import {parseVars} from "../lib";
|
import {parseVars, requireAllCasesHandledFor} from "../lib";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ===[ Command Types ]===
|
* ===[ Command Types ]===
|
||||||
|
@ -34,11 +34,15 @@ const patterns = {
|
||||||
channel: /^<#(\d{17,19})>$/,
|
channel: /^<#(\d{17,19})>$/,
|
||||||
role: /^<@&(\d{17,19})>$/,
|
role: /^<@&(\d{17,19})>$/,
|
||||||
emote: /^<a?:.*?:(\d{17,19})>$/,
|
emote: /^<a?:.*?:(\d{17,19})>$/,
|
||||||
message: /(?:\d{17,19}\/(\d{17,19})\/(\d{17,19})$)|(?:^(\d{17,19})-(\d{17,19})$)/,
|
messageLink: /^https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(?:\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})$/,
|
||||||
|
messagePair: /^(\d{17,19})-(\d{17,19})$/,
|
||||||
user: /^<@!?(\d{17,19})>$/,
|
user: /^<@!?(\d{17,19})>$/,
|
||||||
id: /^(\d{17,19})$/
|
id: /^(\d{17,19})$/
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Maybe add a guild redirect... somehow?
|
||||||
|
type ID = "channel" | "role" | "emote" | "message" | "user";
|
||||||
|
|
||||||
// Callbacks don't work with discriminated unions:
|
// Callbacks don't work with discriminated unions:
|
||||||
// - https://github.com/microsoft/TypeScript/issues/41759
|
// - https://github.com/microsoft/TypeScript/issues/41759
|
||||||
// - https://github.com/microsoft/TypeScript/issues/35769
|
// - https://github.com/microsoft/TypeScript/issues/35769
|
||||||
|
@ -78,10 +82,17 @@ interface CommandOptionsEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevents subcommands from being added by compile-time.
|
// Prevents subcommands from being added by compile-time.
|
||||||
|
// Also, contrary to what you might think, channel pings do still work in DM channels.
|
||||||
|
// Role pings, maybe not, but it's not a big deal.
|
||||||
interface CommandOptionsNonEndpoint {
|
interface CommandOptionsNonEndpoint {
|
||||||
readonly endpoint?: false;
|
readonly endpoint?: false;
|
||||||
readonly subcommands?: {[key: string]: NamedCommand};
|
readonly subcommands?: {[key: string]: NamedCommand};
|
||||||
|
readonly channel?: Command;
|
||||||
|
readonly role?: Command;
|
||||||
|
readonly emote?: Command;
|
||||||
|
readonly message?: Command;
|
||||||
readonly user?: Command;
|
readonly user?: Command;
|
||||||
|
readonly id?: ID;
|
||||||
readonly number?: Command;
|
readonly number?: Command;
|
||||||
readonly any?: Command;
|
readonly any?: Command;
|
||||||
}
|
}
|
||||||
|
@ -119,6 +130,7 @@ interface CommandInfoMetadata {
|
||||||
channelType: CHANNEL_TYPE;
|
channelType: CHANNEL_TYPE;
|
||||||
args: string[];
|
args: string[];
|
||||||
usage: string;
|
usage: string;
|
||||||
|
readonly originalArgs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultMetadata = {
|
export const defaultMetadata = {
|
||||||
|
@ -136,7 +148,13 @@ export class Command {
|
||||||
public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit
|
public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit
|
||||||
protected run: (($: CommandMenu) => Promise<any>) | string;
|
protected run: (($: CommandMenu) => Promise<any>) | string;
|
||||||
protected readonly subcommands: Collection<string, NamedCommand>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
|
protected readonly subcommands: Collection<string, NamedCommand>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
|
||||||
|
protected channel: Command | null;
|
||||||
|
protected role: Command | null;
|
||||||
|
protected emote: Command | null;
|
||||||
|
protected message: Command | null;
|
||||||
protected user: Command | null;
|
protected user: Command | null;
|
||||||
|
protected id: Command | null;
|
||||||
|
protected idType: ID | null;
|
||||||
protected number: Command | null;
|
protected number: Command | null;
|
||||||
protected any: Command | null;
|
protected any: Command | null;
|
||||||
|
|
||||||
|
@ -149,14 +167,47 @@ export class Command {
|
||||||
this.channelType = options?.channelType ?? null;
|
this.channelType = options?.channelType ?? null;
|
||||||
this.run = options?.run || "No action was set on this command!";
|
this.run = options?.run || "No action was set on this command!";
|
||||||
this.subcommands = new Collection(); // Populate this collection after setting subcommands.
|
this.subcommands = new Collection(); // Populate this collection after setting subcommands.
|
||||||
|
this.channel = null;
|
||||||
|
this.role = null;
|
||||||
|
this.emote = null;
|
||||||
|
this.message = null;
|
||||||
this.user = null;
|
this.user = null;
|
||||||
|
this.id = null;
|
||||||
|
this.idType = null;
|
||||||
this.number = null;
|
this.number = null;
|
||||||
this.any = null;
|
this.any = null;
|
||||||
|
|
||||||
if (options && !options.endpoint) {
|
if (options && !options.endpoint) {
|
||||||
this.user = options?.user || null;
|
if (options?.channel) this.channel = options.channel;
|
||||||
this.number = options?.number || null;
|
if (options?.role) this.role = options.role;
|
||||||
this.any = options?.any || null;
|
if (options?.emote) this.emote = options.emote;
|
||||||
|
if (options?.message) this.message = options.message;
|
||||||
|
if (options?.user) this.user = options.user;
|
||||||
|
if (options?.number) this.number = options.number;
|
||||||
|
if (options?.any) this.any = options.any;
|
||||||
|
if (options?.id) this.idType = options.id;
|
||||||
|
|
||||||
|
if (options?.id) {
|
||||||
|
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:
|
||||||
|
requireAllCasesHandledFor(options.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.subcommands) {
|
if (options?.subcommands) {
|
||||||
const baseSubcommands = Object.keys(options.subcommands);
|
const baseSubcommands = Object.keys(options.subcommands);
|
||||||
|
@ -271,8 +322,85 @@ export class Command {
|
||||||
|
|
||||||
// Resolve the value of the current command's argument (adding it to the resolved args),
|
// Resolve the value of the current command's argument (adding it to the resolved args),
|
||||||
// then pass the thread of execution to whichever subcommand is valid (if any).
|
// then pass the thread of execution to whichever subcommand is valid (if any).
|
||||||
|
const isMessageLink = patterns.messageLink.test(param);
|
||||||
|
const isMessagePair = patterns.messagePair.test(param);
|
||||||
|
|
||||||
if (this.subcommands.has(param)) {
|
if (this.subcommands.has(param)) {
|
||||||
return this.subcommands.get(param)!.execute(args, menu, metadata);
|
return this.subcommands.get(param)!.execute(args, menu, metadata);
|
||||||
|
} else if (this.channel && patterns.channel.test(param)) {
|
||||||
|
const id = patterns.channel.exec(param)![1];
|
||||||
|
const channel = menu.client.channels.cache.get(id);
|
||||||
|
|
||||||
|
// Users can only enter in this format for text channels, so this restricts it to that.
|
||||||
|
if (channel instanceof TextChannel) {
|
||||||
|
menu.args.push(channel);
|
||||||
|
return this.channel.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` is not a valid text channel!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (this.role && patterns.role.test(param)) {
|
||||||
|
const id = patterns.role.exec(param)![1];
|
||||||
|
|
||||||
|
if (!menu.guild) {
|
||||||
|
return {
|
||||||
|
content: "You can't use role parameters in DM channels!"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = menu.guild.roles.cache.get(id);
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
menu.args.push(role);
|
||||||
|
return this.role.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` is not a valid role in this server!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (this.emote && patterns.emote.test(param)) {
|
||||||
|
const id = patterns.emote.exec(param)![1];
|
||||||
|
const emote = menu.client.emojis.cache.get(id);
|
||||||
|
|
||||||
|
if (emote) {
|
||||||
|
menu.args.push(emote);
|
||||||
|
return this.emote.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` isn't a valid emote!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (this.message && (isMessageLink || isMessagePair)) {
|
||||||
|
let channelID = "";
|
||||||
|
let messageID = "";
|
||||||
|
|
||||||
|
if (isMessageLink) {
|
||||||
|
const result = patterns.messageLink.exec(param)!;
|
||||||
|
channelID = result[1];
|
||||||
|
messageID = result[2];
|
||||||
|
} else if (isMessagePair) {
|
||||||
|
const result = patterns.messagePair.exec(param)!;
|
||||||
|
channelID = result[1];
|
||||||
|
messageID = result[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = menu.client.channels.cache.get(channelID);
|
||||||
|
|
||||||
|
if (channel instanceof TextChannel || channel instanceof DMChannel) {
|
||||||
|
try {
|
||||||
|
menu.args.push(await channel.messages.fetch(messageID));
|
||||||
|
return this.message.execute(args, menu, metadata);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
content: `\`${messageID}\` isn't a valid message of channel ${channel}!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${channelID}\` is not a valid text channel!`
|
||||||
|
};
|
||||||
|
}
|
||||||
} else if (this.user && patterns.user.test(param)) {
|
} else if (this.user && patterns.user.test(param)) {
|
||||||
const id = patterns.user.exec(param)![1];
|
const id = patterns.user.exec(param)![1];
|
||||||
|
|
||||||
|
@ -284,6 +412,73 @@ export class Command {
|
||||||
content: `No user found by the ID \`${id}\`!`
|
content: `No user found by the ID \`${id}\`!`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (this.id && this.idType && patterns.id.test(param)) {
|
||||||
|
const id = patterns.id.exec(param)![1];
|
||||||
|
|
||||||
|
// Probably modularize the findXByY code in general in libd.
|
||||||
|
// Because this part is pretty much a whole bunch of copy pastes.
|
||||||
|
switch (this.idType) {
|
||||||
|
case "channel":
|
||||||
|
const channel = menu.client.channels.cache.get(id);
|
||||||
|
|
||||||
|
// Users can only enter in this format for text channels, so this restricts it to that.
|
||||||
|
if (channel instanceof TextChannel) {
|
||||||
|
menu.args.push(channel);
|
||||||
|
return this.id.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` isn't a valid text channel!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "role":
|
||||||
|
if (!menu.guild) {
|
||||||
|
return {
|
||||||
|
content: "You can't use role parameters in DM channels!"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = menu.guild.roles.cache.get(id);
|
||||||
|
|
||||||
|
if (role) {
|
||||||
|
menu.args.push(role);
|
||||||
|
return this.id.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` isn't a valid role in this server!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "emote":
|
||||||
|
const emote = menu.client.emojis.cache.get(id);
|
||||||
|
|
||||||
|
if (emote) {
|
||||||
|
menu.args.push(emote);
|
||||||
|
return this.id.execute(args, menu, metadata);
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` isn't a valid emote!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "message":
|
||||||
|
try {
|
||||||
|
menu.args.push(await menu.channel.messages.fetch(id));
|
||||||
|
return this.id.execute(args, menu, metadata);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
content: `\`${id}\` isn't a valid message of channel ${menu.channel}!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "user":
|
||||||
|
try {
|
||||||
|
menu.args.push(await menu.client.users.fetch(id));
|
||||||
|
return this.id.execute(args, menu, metadata);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
content: `No user found by the ID \`${id}\`!`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
requireAllCasesHandledFor(this.idType);
|
||||||
|
}
|
||||||
} else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") {
|
} else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") {
|
||||||
menu.args.push(Number(param));
|
menu.args.push(Number(param));
|
||||||
return this.number.execute(args, menu, metadata);
|
return this.number.execute(args, menu, metadata);
|
||||||
|
@ -302,7 +497,7 @@ export class Command {
|
||||||
|
|
||||||
// What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
|
// What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands.
|
||||||
public async resolveInfo(args: string[]): Promise<CommandInfo | CommandInfoError> {
|
public async resolveInfo(args: string[]): Promise<CommandInfo | CommandInfoError> {
|
||||||
return this.resolveInfoInternal(args, {...defaultMetadata, args: [], usage: ""});
|
return this.resolveInfoInternal(args, {...defaultMetadata, args: [], usage: "", originalArgs: [...args]});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveInfoInternal(
|
private async resolveInfoInternal(
|
||||||
|
@ -333,7 +528,12 @@ export class Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then get all the generic subcommands.
|
// Then get all the generic subcommands.
|
||||||
|
if (this.channel) subcommandInfo.set("<channel>", this.channel);
|
||||||
|
if (this.role) subcommandInfo.set("<role>", this.role);
|
||||||
|
if (this.emote) subcommandInfo.set("<emote>", this.emote);
|
||||||
|
if (this.message) subcommandInfo.set("<message>", this.message);
|
||||||
if (this.user) subcommandInfo.set("<user>", this.user);
|
if (this.user) subcommandInfo.set("<user>", this.user);
|
||||||
|
if (this.id) subcommandInfo.set(`<id = <${this.idType}>>`, this.id);
|
||||||
if (this.number) subcommandInfo.set("<number>", this.number);
|
if (this.number) subcommandInfo.set("<number>", this.number);
|
||||||
if (this.any) subcommandInfo.set("<any>", this.any);
|
if (this.any) subcommandInfo.set("<any>", this.any);
|
||||||
|
|
||||||
|
@ -346,45 +546,73 @@ export class Command {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const invalidSubcommandGenerator: () => CommandInfoError = () => ({
|
||||||
|
type: "error",
|
||||||
|
message: `No subcommand found by the argument list: \`${metadata.originalArgs.join(" ")}\``
|
||||||
|
});
|
||||||
|
|
||||||
// Then test if anything fits any hardcoded values, otherwise check if it's a valid keyed subcommand.
|
// Then test if anything fits any hardcoded values, otherwise check if it's a valid keyed subcommand.
|
||||||
if (param === "<user>") {
|
if (param === "<channel>") {
|
||||||
|
if (this.channel) {
|
||||||
|
metadata.args.push("<channel>");
|
||||||
|
return this.channel.resolveInfoInternal(args, metadata);
|
||||||
|
} else {
|
||||||
|
return invalidSubcommandGenerator();
|
||||||
|
}
|
||||||
|
} else if (param === "<role>") {
|
||||||
|
if (this.role) {
|
||||||
|
metadata.args.push("<role>");
|
||||||
|
return this.role.resolveInfoInternal(args, metadata);
|
||||||
|
} else {
|
||||||
|
return invalidSubcommandGenerator();
|
||||||
|
}
|
||||||
|
} else if (param === "<emote>") {
|
||||||
|
if (this.emote) {
|
||||||
|
metadata.args.push("<emote>");
|
||||||
|
return this.emote.resolveInfoInternal(args, metadata);
|
||||||
|
} else {
|
||||||
|
return invalidSubcommandGenerator();
|
||||||
|
}
|
||||||
|
} else if (param === "<message>") {
|
||||||
|
if (this.message) {
|
||||||
|
metadata.args.push("<message>");
|
||||||
|
return this.message.resolveInfoInternal(args, metadata);
|
||||||
|
} else {
|
||||||
|
return invalidSubcommandGenerator();
|
||||||
|
}
|
||||||
|
} else if (param === "<user>") {
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
metadata.args.push("<user>");
|
metadata.args.push("<user>");
|
||||||
return this.user.resolveInfoInternal(args, metadata);
|
return this.user.resolveInfoInternal(args, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return invalidSubcommandGenerator();
|
||||||
type: "error",
|
}
|
||||||
message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\``
|
} else if (param === "<id>") {
|
||||||
};
|
if (this.id) {
|
||||||
|
metadata.args.push(`<id = <${this.idType}>>`);
|
||||||
|
return this.id.resolveInfoInternal(args, metadata);
|
||||||
|
} else {
|
||||||
|
return invalidSubcommandGenerator();
|
||||||
}
|
}
|
||||||
} else if (param === "<number>") {
|
} else if (param === "<number>") {
|
||||||
if (this.number) {
|
if (this.number) {
|
||||||
metadata.args.push("<number>");
|
metadata.args.push("<number>");
|
||||||
return this.number.resolveInfoInternal(args, metadata);
|
return this.number.resolveInfoInternal(args, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return invalidSubcommandGenerator();
|
||||||
type: "error",
|
|
||||||
message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\``
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else if (param === "<any>") {
|
} else if (param === "<any>") {
|
||||||
if (this.any) {
|
if (this.any) {
|
||||||
metadata.args.push("<any>");
|
metadata.args.push("<any>");
|
||||||
return this.any.resolveInfoInternal(args, metadata);
|
return this.any.resolveInfoInternal(args, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return invalidSubcommandGenerator();
|
||||||
type: "error",
|
|
||||||
message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\``
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} else if (this.subcommands?.has(param)) {
|
} else if (this.subcommands?.has(param)) {
|
||||||
metadata.args.push(param);
|
metadata.args.push(param);
|
||||||
return this.subcommands.get(param)!.resolveInfoInternal(args, metadata);
|
return this.subcommands.get(param)!.resolveInfoInternal(args, metadata);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return invalidSubcommandGenerator();
|
||||||
type: "error",
|
|
||||||
message: `No subcommand found by the argument list: \`${metadata.args.join(" ")}\``
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,16 @@ if (IS_DEV_MODE && !exists("src/commands/test.ts")) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A generic process handler is set to catch unhandled rejections other than the ones from Lavalink and Discord.
|
||||||
|
process.on("unhandledRejection", (reason: any) => {
|
||||||
|
const isLavalinkError = reason?.code === "ECONNREFUSED";
|
||||||
|
const isDiscordError = reason?.name === "DiscordAPIError";
|
||||||
|
|
||||||
|
if (!isLavalinkError && !isDiscordError) {
|
||||||
|
console.error(reason.stack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
|
// This file is called (or at least should be called) automatically as long as a config file doesn't exist yet.
|
||||||
// And that file won't be written until the data is successfully initialized.
|
// And that file won't be written until the data is successfully initialized.
|
||||||
const prompts = [
|
const prompts = [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue