From 44cae5c0cba0be0209ff8700d56b4410af38a8ce Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Mon, 5 Apr 2021 03:46:50 -0500 Subject: [PATCH] Fixed some bugs and added proper event handler --- src/commands/system/help.ts | 32 +++++++++++++++++++++++++++----- src/core/command.ts | 31 +++++++++++++++++-------------- src/core/handler.ts | 31 ++++++++++++++++++++++++++++++- src/core/index.ts | 2 +- src/modules/lavalink.ts | 24 ++++++++++++++++++++++++ 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index e3b160c..d205a10 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -1,5 +1,5 @@ -import {Command, NamedCommand, loadableCommands, categories, getPermissionName} from "../../core"; -import {toTitleCase} from "../../lib"; +import {Command, NamedCommand, loadableCommands, categories, getPermissionName, CHANNEL_TYPE} from "../../core"; +import {toTitleCase, requireAllCasesHandledFor} from "../../lib"; export default new NamedCommand({ description: "Lists all commands. If a command is specified, their arguments are listed as well.", @@ -10,14 +10,19 @@ export default new NamedCommand({ let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\``; for (const [category, headers] of categories) { - output += `\n\n===[ ${toTitleCase(category)} ]===`; + let tmp = `\n\n===[ ${toTitleCase(category)} ]===`; + // Ignore empty categories, including ["test"]. + let hasActualCommands = false; for (const header of headers) { if (header !== "test") { const command = commands.get(header)!; - output += `\n- \`${header}\`: ${command.description}`; + tmp += `\n- \`${header}\`: ${command.description}`; + hasActualCommands = true; } } + + if (hasActualCommands) output += tmp; } channel.send(output, {split: true}); @@ -50,6 +55,8 @@ export default new NamedCommand({ let append = ""; command = result.command; + if (result.args.length > 0) header += " " + result.args.join(" "); + if (command.usage === "") { const list: string[] = []; @@ -80,9 +87,24 @@ export default new NamedCommand({ return channel.send( `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${category}\`\nPermission Required: \`${getPermissionName( result.permission - )}\` (${result.permission})\nDescription: ${command.description}\n${append}`, + )}\` (${result.permission})\nChannel Type: ${getChannelTypeName(result.channelType)}\nNSFW Only: ${ + result.nsfw ? "Yes" : "No" + }\nDescription: ${command.description}\n${append}`, {split: true} ); } }) }); + +function getChannelTypeName(type: CHANNEL_TYPE): string { + switch (type) { + case CHANNEL_TYPE.ANY: + return "Any"; + case CHANNEL_TYPE.GUILD: + return "Guild Only"; + case CHANNEL_TYPE.DM: + return "DM Only"; + default: + requireAllCasesHandledFor(type); + } +} diff --git a/src/core/command.ts b/src/core/command.ts index becc5f6..d389619 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -32,7 +32,7 @@ const patterns = { // Therefore, there won't by any type narrowing on channel or guild of CommandMenu until this is fixed. // Otherwise, you'd have to define channelType for every single subcommand, which would get very tedious. // Just use type assertions when you specify a channel type. -enum CHANNEL_TYPE { +export enum CHANNEL_TYPE { ANY, GUILD, DM @@ -126,7 +126,6 @@ export class Command { protected user: Command | null; protected number: Command | null; protected any: Command | null; - public static readonly CHANNEL_TYPE = CHANNEL_TYPE; constructor(options?: CommandOptions) { this.description = options?.description || "No description."; @@ -182,6 +181,13 @@ export class Command { menu: CommandMenu, metadata: ExecuteCommandMetadata ): Promise { + // Update inherited properties if the current command specifies a property. + // In case there are no initial arguments, these should go first so that it can register. + if (this.permission !== -1) metadata.permission = this.permission; + if (this.nsfw !== null) metadata.nsfw = this.nsfw; + if (this.channelType !== null) metadata.channelType = this.channelType; + + // Take off the leftmost argument from the list. const param = args.shift(); // If there are no arguments left, execute the current command. Otherwise, continue on. @@ -239,7 +245,7 @@ export class Command { return null; } catch (error) { const errorMessage = error.stack ?? error; - console.error(`Command Error: ${metadata.header} (${metadata.args})\n${errorMessage}`); + console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); return { content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` @@ -250,11 +256,6 @@ export class Command { // If the current command is an endpoint but there are still some arguments left, don't continue. if (this.endpoint) return {content: "Too many arguments!"}; - // Update inherited properties if the current command specifies a property. - if (this.permission !== -1) metadata.permission = this.permission; - if (this.nsfw !== null) metadata.nsfw = this.nsfw; - if (this.channelType !== null) metadata.channelType = this.channelType; - // 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). if (this.subcommands.has(param)) { @@ -295,6 +296,14 @@ export class Command { args: string[], metadata: CommandInfoMetadata ): Promise { + // Update inherited properties if the current command specifies a property. + // In case there are no initial arguments, these should go first so that it can register. + if (this.permission !== -1) metadata.permission = this.permission; + if (this.nsfw !== null) metadata.nsfw = this.nsfw; + if (this.channelType !== null) metadata.channelType = this.channelType; + if (this.usage !== "") metadata.usage = this.usage; + + // Take off the leftmost argument from the list. const param = args.shift(); // If there are no arguments left, return the data or an error message. @@ -324,12 +333,6 @@ export class Command { }; } - // Update inherited properties if the current command specifies a property. - if (this.permission !== -1) metadata.permission = this.permission; - if (this.nsfw !== null) metadata.nsfw = this.nsfw; - if (this.channelType !== null) metadata.channelType = this.channelType; - if (this.usage !== "") metadata.usage = this.usage; - // Then test if anything fits any hardcoded values, otherwise check if it's a valid keyed subcommand. if (param === "") { if (this.user) { diff --git a/src/core/handler.ts b/src/core/handler.ts index 0646394..10bea9e 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -1,6 +1,6 @@ import {client} from "../index"; import {loadableCommands} from "./loader"; -import {Permissions, Message} from "discord.js"; +import {Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; import {getPrefix} from "../structures"; import {defaultMetadata} from "./command"; @@ -11,6 +11,16 @@ export function addInterceptRule(handler: (message: Message) => boolean) { interceptRules.push(handler); } +const lastCommandInfo: { + header: string; + args: string[]; + channel: TextChannel | DMChannel | NewsChannel | null; +} = { + header: "N/A", + args: [], + channel: null +}; + // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined. // Note: guild.available will never need to be checked because the message starts in either a DM channel or an already-available guild. client.on("message", async (message) => { @@ -41,6 +51,11 @@ client.on("message", async (message) => { if (commands.has(header)) { const command = commands.get(header)!; + // Set last command info in case of unhandled rejections. + lastCommandInfo.header = header; + lastCommandInfo.args = [...args]; + lastCommandInfo.channel = channel; + // Send the arguments to the command to resolve and execute. const result = await command.execute(args, menu, { header, @@ -73,6 +88,11 @@ client.on("message", async (message) => { if (commands.has(header)) { const command = commands.get(header)!; + // Set last command info in case of unhandled rejections. + lastCommandInfo.header = header; + lastCommandInfo.args = [...args]; + lastCommandInfo.channel = channel; + // Send the arguments to the command to resolve and execute. const result = await command.execute(args, menu, { header, @@ -98,3 +118,12 @@ client.on("message", async (message) => { ); } }); + +process.on("unhandledRejection", (reason: any) => { + if (reason?.name === "DiscordAPIError") { + console.error(`Command Error: ${lastCommandInfo.header} (${lastCommandInfo.args.join(", ")})\n${reason.stack}`); + lastCommandInfo.channel?.send( + `There was an error while trying to execute that command!\`\`\`${reason.stack}\`\`\`` + ); + } +}); diff --git a/src/core/index.ts b/src/core/index.ts index 16b06e0..047e6f5 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,4 +1,4 @@ -export {Command, NamedCommand} from "./command"; +export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; export {addInterceptRule} from "./handler"; export { SingleMessageOptions, diff --git a/src/modules/lavalink.ts b/src/modules/lavalink.ts index 41a24fb..8b18a7a 100644 --- a/src/modules/lavalink.ts +++ b/src/modules/lavalink.ts @@ -22,3 +22,27 @@ attachClientToLavalink(client, { helpCmd: "mhelp", admins: ["717352467280691331"] }); + +// Disable the unhandledRejection listener by Lavalink because it captures every single unhandled +// rejection and adds its message with it. Then replace it with a better, more selective error handler. +for (const listener of process.listeners("unhandledRejection")) { + if (listener.toString().includes("discord.js-lavalink-musicbot")) { + process.off("unhandledRejection", listener); + } +} + +process.on("unhandledRejection", (reason: any) => { + if (reason?.code === "ECONNREFUSED") { + console.error( + `[discord.js-lavalink-musicbot] Caught unhandled rejection: ${reason.stack}\nIf this is causing issues, head to the support server at https://discord.gg/dNN4azK` + ); + } +}); + +// It's unsafe to process uncaughtException because after an uncaught exception, the system +// becomes corrupted. So disable Lavalink from adding a hook to it. +for (const listener of process.listeners("uncaughtException")) { + if (listener.toString().includes("discord.js-lavalink-musicbot")) { + process.off("uncaughtException", listener); + } +}