From b8e15fe9c302334f3a63d92183da3dbd86968b5a Mon Sep 17 00:00:00 2001 From: "Zaid Arshad (Nico)" Date: Tue, 9 Apr 2024 17:50:51 -0400 Subject: [PATCH] feat: more responses --- .../gil/__tests__/shared/commands/Ping.ts | 1 - packages/gil/lib/GilClient.ts | 5 +- packages/gil/lib/arguments/ArgumentParser.ts | 4 +- .../lib/listeners/CommandMessageListener.ts | 21 +++- packages/gil/lib/structures/Command.ts | 4 +- packages/gil/lib/structures/Responses.ts | 101 ++++++++++++++++++ packages/gil/lib/types.ts | 2 +- packages/gil/lib/utils/string.ts | 14 +++ 8 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 packages/gil/lib/utils/string.ts diff --git a/packages/gil/__tests__/shared/commands/Ping.ts b/packages/gil/__tests__/shared/commands/Ping.ts index 45bd82fc..eba18842 100644 --- a/packages/gil/__tests__/shared/commands/Ping.ts +++ b/packages/gil/__tests__/shared/commands/Ping.ts @@ -4,7 +4,6 @@ export default class Ping extends Command { options = { name: "ping", description: "Tests the bot.", - userRole: StoredRoleType.Admin, } satisfies CommandOptions; public async execute() { diff --git a/packages/gil/lib/GilClient.ts b/packages/gil/lib/GilClient.ts index 449501ff..59bf66d3 100644 --- a/packages/gil/lib/GilClient.ts +++ b/packages/gil/lib/GilClient.ts @@ -14,6 +14,7 @@ import { CommandCustomContextFn, CommandErrorHandler } from "./types"; interface GilClientOptions { token: string; clientOptions?: ClientOptions; + supportServer?: string; contexts?: { command: CommandCustomContextFn; }; @@ -31,6 +32,7 @@ interface GilClientOptions { // other operators?: string[]; premiumPrioritys?: string[]; + idGenerator?: () => string; } export class GilClient { public readonly client = new Client({ @@ -47,6 +49,7 @@ export class GilClient { ...defaultResponses, ...(this.options.responses ?? {}), }; + public readonly idGenerator = this.options.idGenerator ?? (() => crypto.randomUUID()); public constructor(public options: GilClientOptions) { if (!options.token) throw new Error("No token provided"); @@ -67,7 +70,7 @@ export class GilClient { channel: { channelId: string }, key: T, options?: { - args: DefaultResponseParams[T]["0"]; + args: DefaultResponseParams[T][0]; }, ): Promise { const response = this.responses[key]; diff --git a/packages/gil/lib/arguments/ArgumentParser.ts b/packages/gil/lib/arguments/ArgumentParser.ts index ea755d98..4938ff7c 100644 --- a/packages/gil/lib/arguments/ArgumentParser.ts +++ b/packages/gil/lib/arguments/ArgumentParser.ts @@ -8,7 +8,7 @@ import number from "./args/number"; import role from "./args/role"; import string from "./args/string"; -export type Result = ({ error: false } & T) | { error: true; reason_code: string }; +export type Result = ({ error: false } & T) | { error: true; reason_code: string; extra_info?: unknown }; export type CommandArgument = string | number | boolean | PartialMember | Message | Channel | Role | null; export type CommandArgumentType = "string" | "number" | "boolean" | "member" | "channel" | "role"; export type CommandArgumentValidator = { @@ -55,6 +55,8 @@ export async function convertArguments(params: { castedArguments[currentArg.name] = null; continue; } + + return { error: true, reason_code: "MISSING_ARGUMENT", extra_info: { argument: currentArg } }; } const validator = validators[currentArg.type].validate; diff --git a/packages/gil/lib/listeners/CommandMessageListener.ts b/packages/gil/lib/listeners/CommandMessageListener.ts index 0a6cd7e5..651d648b 100644 --- a/packages/gil/lib/listeners/CommandMessageListener.ts +++ b/packages/gil/lib/listeners/CommandMessageListener.ts @@ -73,7 +73,12 @@ export default class CommandMessageListener extends Listener { if (attemptConvertArguments.error) { this.gil.logger.debug(`Error converting arguments for command ${name}, reason: ${attemptConvertArguments.reason_code}`, params.message.id); - // TODO: in-depth error messages for users + await this.gil.send(params.message, "invalidArguments", { + args: { + reason_code: attemptConvertArguments.reason_code, + extra_info: attemptConvertArguments.extra_info, + }, + }); return; } @@ -98,14 +103,22 @@ export default class CommandMessageListener extends Listener { ...context, }); } catch (e) { - // todo: user friendly error "something went wrong" message - - this.gil.logger.error(e as Error); + const error_id = this.gil.idGenerator(); + + this.gil.send(params.message, "errorCommand", { + args: { + error_message: e instanceof Error ? e.message : "An unknown error occurred", + support_server: this.gil.options.supportServer, + error_id, + }, + }); + this.gil.logger.error(e as Error, error_id); this.gil.logger.warn(`Error executing command ${name}`, params.message.id); this.gil.options.errorHandler?.command?.(e as Error, { message: params.message, member: params.member, server: params.server, + error_id, command, }); } diff --git a/packages/gil/lib/structures/Command.ts b/packages/gil/lib/structures/Command.ts index 38bbe4db..3c8bf3b3 100644 --- a/packages/gil/lib/structures/Command.ts +++ b/packages/gil/lib/structures/Command.ts @@ -13,7 +13,7 @@ export interface CommandOptions { // A brief description of the command description?: string; // The arguments this command takes - args?: { name: string; type: CommandArgumentType; optional?: boolean }[]; + args?: CommandArg[]; // The category the command belongs to category?: string; // The command's aliases @@ -37,6 +37,8 @@ export interface CommandOptions { // The premium level the user must have to run this command premiumUserLevel?: string; } +export type CommandArg = { name: string; type: CommandArgumentType; optional?: boolean }; + export abstract class Command { public constructor( public readonly gil: GilClient, diff --git a/packages/gil/lib/structures/Responses.ts b/packages/gil/lib/structures/Responses.ts index 81c937a7..c2f6de3e 100644 --- a/packages/gil/lib/structures/Responses.ts +++ b/packages/gil/lib/structures/Responses.ts @@ -1,4 +1,6 @@ import { Embed, MessageContent } from "guilded.js"; +import { strip } from "../utils/string"; +import { CommandArg } from "./Command"; export type Response = (...args: any[]) => MessageContent; export type ParamsObject = T extends (...args: infer P) => any ? { [K in keyof P]: P[K] } : never; @@ -12,6 +14,105 @@ export const defaultResponses = { userNotPremium: (p: { tier: string }) => new Embed().setTitle("You are not premium").setDescription(`You do not have premium. To use this command, you must be on the ${p.tier} tier.`), userMissingRole: (p: { requiredRole: string[] }) => new Embed().setTitle("You can't run this!").setDescription(`You do not have a role with the ${inlineCode(p.requiredRole.join(", "))} permission.`), + invalidArguments: (p: { reason_code: string; extra_info: unknown }) => { + const embed = new Embed().setTitle("Invalid Usage!").setColor("RED"); + + switch (p.reason_code) { + case "MISSING_ARGUMENT": { + const extra_info = p.extra_info as { argument: CommandArg }; + embed.setDescription(strip` + You are missing the required argument: ${inlineCode(extra_info.argument.name)}. + + It should be of type ${inlineCode(extra_info.argument.type)}. + `); + break; + } + case "INVALID_NUMBER": + embed.setDescription("I was unable to understand the number you provided.\n\nPlease ensure your numbers are formatted like so: `123`, `111`, or `1e4`. Do not include commands or decimals."); + break; + case "NUMBER_OUT_OF_RANGE": + embed.setDescription("The number you provided is out of acceptable range.\n\nPlease make sure your number is between `-2,147,483,648` and `2,147,483,647`."); + break; + case "BAD_STRING": + embed.setDescription("You provided an invalid string."); + break; + case "INVALID_MEMBER_INPUT": + embed.setDescription(strip` + I was expecting a mention or ID of a user. It may look something like this: \`@user\` or \`pmbOB8VA\` + + This user **must** currently be in the server. + Don't know how to get IDs? Refer to this [Guilded Post](https://support.guilded.gg/hc/en-us/articles/6183962129303-Developer-mode#:~:text=Once%20you've%20enabled%20Developer,by%20right%2Dclicking%20on%20it.). + `); + break; + case "MEMBER_NOT_FOUND": + embed.setDescription(strip` + The user you provided was not found. It may look something like this: \`@user\` or \`pmbOB8VA\` + + This user **must** currently be in the server. + `); + break; + case "NO_USER_IN_MENTIONS": + embed.setDescription("You did not mention a user."); + break; + case "INVALID_ROLE_INPUT": + embed.setDescription(strip` + I was expecting the mention or ID of a role in this server. It may look something like this: \`@role\` or \`28086957\` + + Don't know how to get IDs? Refer to this [Guilded Post](https://support.guilded.gg/hc/en-us/articles/6183962129303-Developer-mode#:~:text=Once%20you've%20enabled%20Developer,by%20right%2Dclicking%20on%20it.). + `); + break; + case "ROLE_NOT_FOUND": + embed.setDescription(strip` + The role you provided was not found. It may look something like this: \`@role\` or \`28086957\` + + This role **must** exist in the server. + `); + break; + case "NO_ROLE_IN_MENTIONS": + embed.setDescription("You did not mention a role."); + break; + case "INVALID_CHANNEL_ETC": + embed.setDescription(strip` + I was expecting either a mention or ID of a channel. It may look something like this: \`#channel\` or \`8942a219-6fde-49f0-8d11-13974df4681c\` + + **The bot must have read, send, & manage permission on the channel** + Ensure **none** of the bot's roles deny these permissions. + + Don't know how to get IDs? Refer to this [Guilded Post](https://support.guilded.gg/hc/en-us/articles/6183962129303-Developer-mode#:~:text=Once%20you've%20enabled%20Developer,by%20right%2Dclicking%20on%20it.). + `); + break; + case "CHANNEL_NOT_FOUND": + embed.setDescription(strip` + The channel you provided was not found. It may look something like this: \`#channel\` or \`8942a219-6fde-49f0-8d11-13974df4681c\` + + This channel **must** exist in the server. + `); + break; + case "NO_CHANNEL_IN_MENTIONS": + embed.setDescription("You did not mention a channel."); + break; + case "INVALID_TIME": + embed.setDescription("You provided an invalid time. Please ensure your time is formatted like so: `1d`, `1h`, `1m`, or `1s`."); + break; + case "INVALID_BOOLEAN": + embed.setDescription("You provided an invalid boolean. Please ensure your input is either `true` or `false`."); + break; + default: + embed.setDescription("You provided invalid arguments."); + break; + } + + return embed; + }, + errorCommand: (p: { error_message: string; error_id: string; support_server?: string }) => + new Embed() + .setTitle("An error occurred") + .setColor("RED") + .setDescription(strip` + There was an error while running this command. If this issue persists, please ${p.support_server ? `[report it here](${p.support_server})` : "report it to the bot owner"}. + + When making your report, please provide this error ID: ${inlineCode(p.error_id)} + `), noop: () => "", } as const; diff --git a/packages/gil/lib/types.ts b/packages/gil/lib/types.ts index 15022940..66aa023b 100644 --- a/packages/gil/lib/types.ts +++ b/packages/gil/lib/types.ts @@ -3,4 +3,4 @@ import type { StoredServer } from "./adapters/db/DatabaseAdapter"; import type { Command } from "./structures/Command"; export type CommandCustomContextFn = (data: { server: StoredServer; message: Message }) => Promise>; -export type CommandErrorHandler = (error: Error, additionalDetails: { message: Message; member: Member; server: StoredServer; command: Command }) => void; +export type CommandErrorHandler = (error: Error, additionalDetails: { message: Message; member: Member; server: StoredServer; command: Command; error_id: string }) => void; diff --git a/packages/gil/lib/utils/string.ts b/packages/gil/lib/utils/string.ts new file mode 100644 index 00000000..a667214e --- /dev/null +++ b/packages/gil/lib/utils/string.ts @@ -0,0 +1,14 @@ +export function strip(strings: TemplateStringsArray, ...values: any[]): string { + let slices: string[] = strings.map((x) => x); + if (strings[0] === "") { + slices = strings.slice(1); + } + const fullString = slices.reduce((acc, str, i) => acc + str + (values[i] || ""), ""); + const trimmedLines = fullString.split("\n").map((x) => x.trim()); + + if (trimmedLines[0] === "") { + trimmedLines.shift(); + } + + return trimmedLines.join("\n"); +}