diff --git a/src/Bot.ts b/src/Bot.ts index cfdb115..a6c6a44 100644 --- a/src/Bot.ts +++ b/src/Bot.ts @@ -75,7 +75,7 @@ export class Bot extends EventEmitter { * @param {string} [token] - app token from Discord Developer Portal * @returns *Promise* */ - async start(port?: number): Promise { + async start(port?: number, register?: boolean): Promise { try { console.log(`\nBot name: ${this.name}`); console.log(`Prefix: ${this.commands.prefix} \n`); @@ -102,16 +102,20 @@ export class Bot extends EventEmitter { process.stdout.write("Connecting to Discord... "); this.client.login(this.token); this.client.on("ready", async () => { - console.log("✔"); - process.stdout.write("Registering commands... "); - this.commands.list.map(async (c) => { - await axios.post( + if (register === undefined || register === true) { + console.log("✔"); + process.stdout.write("Registering commands... "); + await axios.put( `https://discord.com/api/v8/applications/${this.applicationId}/commands`, - c.toCommandObject(), + this.commands.list.map((c) => + !c.guilds ? c.toCommandObject() : {} + ), { headers: { Authorization: `Bot ${this.token}` } } ); - }); - console.log("✔\n"); + console.log("✔\n"); + } else { + console.log("✔\n"); + } this.emit("READY"); }); this.client.on("message", async (m) => { diff --git a/src/Command.ts b/src/Command.ts index fbcb3b3..8bbdb58 100644 --- a/src/Command.ts +++ b/src/Command.ts @@ -6,19 +6,19 @@ import { ParameterResolvable, } from "./types.js"; import { MissingParameterError, PermissionsError } from "./errors.js"; -import { Parameter, ProcessArgument } from "./Parameter.js"; +import { Parameter } from "./Parameter.js"; //CLASSES export class Command { name: string; - parameters: Parameter[]; - aliases: string[]; - keywords: string[]; + parameters?: Parameter[]; + aliases?: string[]; + keywords?: string[]; description: string; - usage: string; + usage?: string; permissionCheck: PermissionCheckTypes; - permissions: Permissions; - guilds: Guild[]; + permissions?: Permissions; + guilds?: Guild[]; visible: boolean; private function: ( message?: Message, @@ -41,7 +41,9 @@ export class Command { */ constructor(options: CommandBuilder) { this.name = options.name.split(" ").join("_"); - this.parameters = options.parameters || []; + this.parameters = options.parameters + ? options.parameters.map((p) => new Parameter(p)) + : undefined; this.aliases = Command.processPhrase(options.aliases); this.keywords = Command.processPhrase(options.keywords); this.description = options.description || "No description"; @@ -50,13 +52,18 @@ export class Command { options.permissionCheck == "ALL" || options.permissionCheck == "ANY" ? options.permissionCheck : "ANY"; - this.permissions = new Permissions(options.permissions || undefined); - this.guilds = options.guilds || []; + 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 (!new RegExp("^[w-]{1,32}$").test(this.name)) { + 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"); + } } /** @@ -69,17 +76,18 @@ export class Command { const memberPermissions: Readonly = message?.member?.permissions || new Permissions(); if ( - this.permissionCheck == "ALL" + !this.permissions || + (this.permissionCheck == "ALL" ? memberPermissions.has(this.permissions, true) - : memberPermissions.any(this.permissions, true) + : memberPermissions.any(this.permissions, true)) ) { let inputArguments: ParameterResolvable[] = cmdParams || []; - if (this.parameters.length > 0) { + if (this.parameters) { this.parameters.map((a, i) => { if (!inputArguments[i] && !a.optional) { throw new MissingParameterError(a); } else if (inputArguments[i]) { - inputArguments[i] = ProcessArgument( + inputArguments[i] = Parameter.process( inputArguments[i] as string, a.type ); @@ -98,14 +106,45 @@ export class Command { } 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: options, }; } - private static processPhrase(phrase?: string | string[]): string[] { + private static processPhrase( + phrase?: string | string[] + ): string[] | undefined { if (Array.isArray(phrase)) { const buff = phrase.map((p) => { return p.split(" ").join("_"); @@ -121,17 +160,18 @@ export class Command { buff.push(phrase.split(" ").join("_")); return buff; } else { - return []; + return undefined; } } private generateUsageFromArguments(): string { let usageTemplate: string = ""; - this.parameters.map((e) => { - usageTemplate += `[${e.name} (${e.type}${ - e.optional ? ", optional" : "" - })] `; - }); + this.parameters && + this.parameters.map((e) => { + usageTemplate += `[${e.name} (${e.type}${ + e.optional ? ", optional" : "" + })] `; + }); return usageTemplate; } } diff --git a/src/CommandManager.ts b/src/CommandManager.ts index b2cb91f..06a6fa3 100644 --- a/src/CommandManager.ts +++ b/src/CommandManager.ts @@ -32,33 +32,37 @@ export class CommandManager { if (c.name == phrase) { command = c; } - c.aliases.map((a) => { - if (a == phrase) { - command = c; - } - }); + c.aliases && + c.aliases.map((a) => { + if (a == phrase) { + command = c; + } + }); break; case "NO_PREFIX": - c.keywords.map((k) => { - if (k == phrase) { - command = c; - } - }); + c.keywords && + c.keywords.map((k) => { + if (k == phrase) { + command = c; + } + }); break; case "ALL": if (c.name == phrase) { command = c; } - c.aliases.map((a) => { - if (a == phrase) { - command = c; - } - }); - c.keywords.map((k) => { - if (k == phrase) { - command = c; - } - }); + c.aliases && + c.aliases.map((a) => { + if (a == phrase) { + command = c; + } + }); + c.keywords && + c.keywords.map((k) => { + if (k == phrase) { + command = c; + } + }); break; } }); @@ -82,25 +86,27 @@ export class CommandManager { `The name "${command.name}" has already been registered as ${nameOccurrence.type} in the "${nameOccurrence.command.name}" command.` ); } - command.aliases.map((a, i, ar) => { - const aliasOccurrence: PhraseOccurrenceData | null = - this.findPhraseOccurrence(a); - if (aliasOccurrence) { - console.warn( - `[⚠ WARN] The name "${a}" is already registered as ${aliasOccurrence.type} in the "${aliasOccurrence.command.name}" command. It will be removed from the "${command.name}" command.` - ); - ar.splice(i, 1); - } - }); - this.list.map((c) => { - command.keywords.map((k, i, a) => { - if (c.keywords.indexOf(k) != -1) { + command.aliases && + command.aliases.map((a, i, ar) => { + const aliasOccurrence: PhraseOccurrenceData | null = + this.findPhraseOccurrence(a); + if (aliasOccurrence) { console.warn( - `[⚠ WARN] The name "${k}" is already a registered KEYWORD for the "${c.name}" command. It will be removed from the "${command.name}" command` + `[⚠ WARN] The name "${a}" is already registered as ${aliasOccurrence.type} in the "${aliasOccurrence.command.name}" command. It will be removed from the "${command.name}" command.` ); - a.splice(i, 1); + ar.splice(i, 1); } }); + this.list.map((c) => { + command.keywords && + command.keywords.map((k, i, a) => { + if (c.keywords && c.keywords.indexOf(k) != -1) { + console.warn( + `[⚠ WARN] The name "${k}" is already a registered KEYWORD for the "${c.name}" command. It will be removed from the "${command.name}" command` + ); + a.splice(i, 1); + } + }); }); this.list.push(command); return true; @@ -159,7 +165,7 @@ export class CommandManager { command: c, type: "NAME", }; - } else if (c.aliases.indexOf(phrase || "") != -1) { + } else if (c.aliases && c.aliases.indexOf(phrase || "") != -1) { returnValue = { command: c, type: "ALIAS", diff --git a/src/Help.ts b/src/Help.ts index b315a94..5fd799e 100644 --- a/src/Help.ts +++ b/src/Help.ts @@ -16,7 +16,7 @@ export class HelpMessage extends Command { description: params.description, parameters: [ { - name: "Command name", + name: "command_name", description: "Name of the command that you want to get details about", optional: true, @@ -45,7 +45,10 @@ export class HelpMessage extends Command { `${cmdManager.prefix}${cmd.name} ${cmd.usage}`, false ); - if (cmd.permissions.toArray(false).length > 0) { + if ( + cmd.permissions && + cmd.permissions.toArray(false).length > 0 + ) { let permList: string = ""; cmd.permissions.toArray(false).map((p) => { permList += p + "\n"; @@ -56,14 +59,14 @@ export class HelpMessage extends Command { false ); } - if (cmd.aliases.length > 0) { + if (cmd.aliases && cmd.aliases.length > 0) { let aList: string = ""; cmd.aliases.map((a) => { aList += a + "\n"; }); helpMsg.addField("Aliases:", aList, false); } - if (cmd.keywords.length > 0) { + if (cmd.keywords && cmd.keywords.length > 0) { let kwrdList: string = ""; cmd.keywords.map((k) => { kwrdList += k + "\n"; diff --git a/src/Parameter.ts b/src/Parameter.ts index 8f649ca..554e08a 100644 --- a/src/Parameter.ts +++ b/src/Parameter.ts @@ -1,35 +1,35 @@ import { ParameterType, ParameterResolvable, Choice } from "./types.js"; -export interface Parameter { +export interface ParameterSchema { name: string; description?: string; optional: boolean; type: ParameterType; choices?: Choice[]; } -export const ProcessArgument = ( - a: string, - type: ParameterType -): ParameterResolvable => { - switch (type) { - /* case "TEXT": - return a; - case "NUMBER": - const numF = parseFloat(a); - if (isNaN(numF)) { - throw new ParameterTypeError(a, type); - } else { - return numF; - } - case "TRUE/FALSE": - if (a.toLowerCase() == "true") { - return true; - } else if (a.toLowerCase() == "false") { - return false; - } else { - throw new ParameterTypeError(a, type); - }*/ - default: - return a; - //throw new TypeError("Invalied type specified"); +export class Parameter { + name: string; + description: string; + optional: boolean; + type: ParameterType; + choices?: Choice[]; + + constructor(options: ParameterSchema) { + this.name = options.name; + this.description = options.description || "No description"; + this.optional = options.optional; + this.type = options.type; + this.choices = options.choices; + if (!/^[\w-]{1,32}$/.test(this.name)) { + throw new Error( + `Parameter name ${this.name} doesn't match the pattern` + ); + } + if (this.description.length > 100) { + throw new Error(`Parameter ${this.name}: Description too long`); + } + return; } -}; + static process(a: string, type: ParameterType): ParameterResolvable { + return a; + } +} diff --git a/src/types.ts b/src/types.ts index 8086ade..5633a7d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,7 @@ import { TextChannel, VoiceChannel, } from "discord.js"; -import { Parameter } from "./Parameter.js"; +import { ParameterSchema } from "./Parameter.js"; import { PermissionResolvable, Message, MessageEmbed } from "discord.js"; import type { Command } from "./Command.js"; @@ -24,7 +24,7 @@ export type GetMode = "ALL" | "PREFIX" | "NO_PREFIX"; export type PermissionCheckTypes = "ALL" | "ANY"; export interface CommandBuilder { name: string; - parameters?: Parameter[]; + parameters?: ParameterSchema[]; aliases?: string[] | string; keywords?: string[] | string; description?: string;