Archived
286 lines
12 KiB
TypeScript
286 lines
12 KiB
TypeScript
//IMPORTS
|
|
import {
|
|
Permissions,
|
|
Message,
|
|
MessageEmbed,
|
|
CommandInteraction,
|
|
GuildMember,
|
|
ReplyMessageOptions,
|
|
} from "discord.js";
|
|
import {
|
|
CommandBuilder,
|
|
ParameterResolvable,
|
|
PermissionCheckTypes,
|
|
} from "./types.js";
|
|
import { PermissionsError } from "./errors.js";
|
|
import { DefaultParameter, InputParameter, Parameter } from "./Parameter.js";
|
|
|
|
//CLASSES
|
|
export class Command {
|
|
name: string;
|
|
parameters: Parameter[];
|
|
aliases?: string[];
|
|
keywords?: string[];
|
|
description: string;
|
|
usage?: string;
|
|
permissionCheck: PermissionCheckTypes;
|
|
permissions?: Permissions;
|
|
guilds?: string[];
|
|
visible: boolean;
|
|
private function: (
|
|
params: (
|
|
query: string,
|
|
returnType?: "value" | "object"
|
|
) => ParameterResolvable | InputParameter | null,
|
|
interaction?: Message | CommandInteraction
|
|
) =>
|
|
| void
|
|
| string
|
|
| MessageEmbed
|
|
| ReplyMessageOptions
|
|
| Promise<void | string | MessageEmbed | ReplyMessageOptions>;
|
|
|
|
/**
|
|
* Command constructor
|
|
* @constructor
|
|
* @param {CommandBuilder} options - all command properties
|
|
* @param {string} options.name - command name (used to trigger the command)
|
|
* @param {string | string[]} [options.aliases] - other words that can trigger the command with prefix (not used in slash commands)
|
|
* @param {string | string[]} [options.keywords] - other words that can trigger the command without prefix (not used in slash commands)
|
|
* @param {string} [options.description="No description"] - command description shown in the help message
|
|
* @param {string} [options.usage] - command usage description shown in the help message (usage message can be automatically generated using parameters)
|
|
* @param {PermissionCheckTypes} [options.permissionCheck='ANY'] - specifies if the caller has to have all of the specified permissions or any of that
|
|
* @param {PermissionResolvable} [options.permissions] - permissions needed to run the command
|
|
* @param {boolean} [options.visible=true] - show command in the help message (visible as a slash command)
|
|
* @param {Function} options.function - function that will trigger when the commands gets called
|
|
*/
|
|
constructor(options: CommandBuilder) {
|
|
this.name = options.name.split(" ").join("_");
|
|
if (options.parameters == "no_input" || !options.parameters) {
|
|
this.parameters = [];
|
|
} else if (options.parameters == "simple") {
|
|
this.parameters = [new DefaultParameter()];
|
|
} else {
|
|
this.parameters = options.parameters.map((ps) => new Parameter(ps));
|
|
}
|
|
this.aliases = Command.processPhrase(options.aliases);
|
|
this.keywords = Command.processPhrase(options.keywords);
|
|
this.description = options.description || "No description";
|
|
this.usage = options.usage || this.generateUsageFromArguments();
|
|
this.permissionCheck =
|
|
options.permissionCheck == "ALL" || options.permissionCheck == "ANY"
|
|
? options.permissionCheck
|
|
: "ANY";
|
|
this.permissions = options.permissions
|
|
? new Permissions(options.permissions)
|
|
: undefined;
|
|
this.guilds = options.guilds;
|
|
this.visible = options.visible != undefined ? options.visible : true;
|
|
this.function = options.function;
|
|
if (!/^[\w-]{1,32}$/.test(this.name)) {
|
|
throw new Error(`Incorrect command name: ${this.name}`);
|
|
}
|
|
if (this.description.length > 100) {
|
|
throw new Error("Command description is too long");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the command
|
|
* @param {Message} [message] - a *Message* object used to check caller's permissions. It will get passed to the execution function (specified in *function* property of command's constructor)
|
|
* @param {string[]} [cmdParams] - list of processed parameters passed in a Discord message
|
|
* @returns *Promise<void>*
|
|
*/
|
|
async start(
|
|
interaction?: Message | CommandInteraction,
|
|
cmdParams?: InputParameter[]
|
|
): Promise<void> {
|
|
const paramFindFn = function (
|
|
query: string,
|
|
returnType?: "value" | "object"
|
|
) {
|
|
return returnType === "object"
|
|
? cmdParams?.find((p) => p.name === query) || null
|
|
: cmdParams?.find((p) => p.name === query)?.value || null;
|
|
};
|
|
const memberPermissions: Readonly<Permissions> =
|
|
(interaction?.member?.permissions as Permissions) ||
|
|
new Permissions();
|
|
if (
|
|
!this.permissions ||
|
|
(this.permissionCheck == "ALL"
|
|
? memberPermissions.has(this.permissions, true)
|
|
: memberPermissions.any(this.permissions, true))
|
|
) {
|
|
if (interaction instanceof CommandInteraction) {
|
|
setTimeout(async () => {
|
|
if (!interaction.replied) {
|
|
await interaction.reply({
|
|
embeds: [
|
|
new MessageEmbed()
|
|
.setColor("#ffff00")
|
|
.setTitle("🔄 Please wait..."),
|
|
],
|
|
});
|
|
}
|
|
}, 2000);
|
|
}
|
|
const fnResult = await this.function.call(
|
|
this,
|
|
paramFindFn,
|
|
interaction
|
|
);
|
|
if (interaction instanceof Message) {
|
|
if (
|
|
fnResult instanceof Object &&
|
|
("content" in (fnResult as any) ||
|
|
"embeds" in (fnResult as any) ||
|
|
"files" in (fnResult as any) ||
|
|
"components" in (fnResult as any) ||
|
|
"sticker" in (fnResult as any))
|
|
) {
|
|
await interaction.reply(fnResult as ReplyMessageOptions);
|
|
} else if (typeof fnResult == "string") {
|
|
await interaction?.reply(fnResult);
|
|
} else if (fnResult instanceof MessageEmbed) {
|
|
await interaction?.channel.send({ embeds: [fnResult] });
|
|
}
|
|
} else if (interaction instanceof CommandInteraction) {
|
|
try {
|
|
if (
|
|
fnResult instanceof Object &&
|
|
("content" in (fnResult as any) ||
|
|
"embeds" in (fnResult as any) ||
|
|
"files" in (fnResult as any) ||
|
|
"components" in (fnResult as any) ||
|
|
"sticker" in (fnResult as any))
|
|
) {
|
|
interaction.replied
|
|
? await interaction.editReply(
|
|
fnResult as ReplyMessageOptions
|
|
)
|
|
: await interaction.reply(
|
|
fnResult as ReplyMessageOptions
|
|
);
|
|
} else if (typeof fnResult == "string") {
|
|
interaction.replied
|
|
? await interaction.editReply({
|
|
content: fnResult,
|
|
embeds: [],
|
|
})
|
|
: await interaction.reply({
|
|
content: fnResult,
|
|
embeds: [],
|
|
});
|
|
} else if (fnResult instanceof MessageEmbed) {
|
|
interaction.replied
|
|
? await interaction.editReply({
|
|
embeds: [fnResult],
|
|
})
|
|
: await interaction.reply({
|
|
embeds: [fnResult],
|
|
});
|
|
} else {
|
|
interaction.replied
|
|
? await interaction.editReply({
|
|
embeds: [
|
|
new MessageEmbed()
|
|
.setColor("#00ff00")
|
|
.setTitle(
|
|
"✅ Task completed successfully"
|
|
),
|
|
],
|
|
})
|
|
: await interaction.reply({
|
|
embeds: [
|
|
new MessageEmbed()
|
|
.setColor("#00ff00")
|
|
.setTitle(
|
|
"✅ Task completed successfully"
|
|
),
|
|
],
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.error("[❌ ERROR] Cannot reply.", e);
|
|
}
|
|
}
|
|
} else {
|
|
throw new PermissionsError(
|
|
this,
|
|
interaction?.member as GuildMember
|
|
);
|
|
}
|
|
}
|
|
|
|
toCommandObject() {
|
|
let options: any[] = [];
|
|
if (this.parameters) {
|
|
options = this.parameters.map((p) => {
|
|
let type = 3;
|
|
if (!/^[\w-]{1,32}$/.test(p.name)) {
|
|
throw new Error(
|
|
`Failed to register ${p.name} parameter for ${this.name}: The option name is incorrect`
|
|
);
|
|
}
|
|
if ((p.description || "No description").length > 100) {
|
|
throw new Error(
|
|
`Failed to register ${p.name} parameter for ${this.name}: The description is too long`
|
|
);
|
|
}
|
|
if (p.type == "boolean") type = 5;
|
|
else if (p.type == "user") type = 6;
|
|
else if (p.type == "channel") type = 7;
|
|
else if (p.type == "role") type = 8;
|
|
else if (p.type == "mentionable") type = 9;
|
|
else if (p.type == "number") type = 10;
|
|
return {
|
|
name: p.name,
|
|
description: p.description || "No description",
|
|
required: !p.optional,
|
|
type: type,
|
|
choices: p.choices || [],
|
|
};
|
|
});
|
|
}
|
|
return {
|
|
name: this.name,
|
|
description: this.description,
|
|
options: options,
|
|
};
|
|
}
|
|
|
|
private static processPhrase(
|
|
phrase?: string | string[]
|
|
): string[] | undefined {
|
|
if (Array.isArray(phrase)) {
|
|
const buff = phrase.map((p) => {
|
|
return p.split(" ").join("_");
|
|
});
|
|
buff.map((e, i, a) => {
|
|
if (e == "" || e == " ") {
|
|
a.splice(i, 1);
|
|
}
|
|
});
|
|
return buff;
|
|
} else if (typeof phrase == "string" && phrase != "" && phrase != " ") {
|
|
const buff = [];
|
|
buff.push(phrase.split(" ").join("_"));
|
|
return buff;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
private generateUsageFromArguments(): string {
|
|
let usageTemplate: string = "";
|
|
this.parameters &&
|
|
this.parameters.map((e) => {
|
|
usageTemplate += `[${e.name} (${e.type}${
|
|
e.optional ? ", optional" : ""
|
|
})] `;
|
|
});
|
|
return usageTemplate;
|
|
}
|
|
}
|