From b29a217f76483e000bd9d75d30250d11a0a88c4d Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Thu, 19 Aug 2021 12:03:21 -0400 Subject: added steal command, revamped error handling and other stuff --- src/commands/info/invite.ts | 34 -- src/commands/info/links.ts | 45 +++ src/commands/info/pronouns.ts | 14 +- src/commands/info/snowflakeInfo.ts | 11 +- src/commands/info/userInfo.ts | 19 +- src/commands/moderation/ban.ts | 1 + src/commands/utilities/activity.ts | 57 +-- src/commands/utilities/steal.ts | 59 +++ src/commands/utilities/whoHasRole.ts | 3 +- .../extensions/discord-akairo/BushClientUtil.ts | 419 ++++++++++----------- src/lib/extensions/discord.js/BushClientUser.d.ts | 13 +- src/lib/utils/BushConstants.ts | 64 ++++ src/lib/utils/BushLogger.ts | 7 +- src/listeners/commands/commandError.ts | 146 ++++--- src/listeners/other/promiseRejection.ts | 10 +- src/listeners/other/uncaughtException.ts | 10 +- 16 files changed, 554 insertions(+), 358 deletions(-) delete mode 100644 src/commands/info/invite.ts create mode 100644 src/commands/info/links.ts create mode 100644 src/commands/utilities/steal.ts (limited to 'src') diff --git a/src/commands/info/invite.ts b/src/commands/info/invite.ts deleted file mode 100644 index 615e767..0000000 --- a/src/commands/info/invite.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BushCommand, BushMessage, BushSlashMessage } from '@lib'; -import { MessageActionRow, MessageButton } from 'discord.js'; - -export default class InviteCommand extends BushCommand { - public constructor() { - super('invite', { - aliases: ['invite'], - category: 'info', - description: { - content: 'Sends the bot invite link.', - usage: 'invite', - examples: ['invite'] - }, - ratelimit: 4, - cooldown: 4000, - clientPermissions: ['SEND_MESSAGES'], - slash: true - }); - } - - public override async exec(message: BushMessage | BushSlashMessage): Promise { - if (client.config.isDevelopment) return await message.util.reply(`${util.emojis.error} The dev bot cannot be invited.`); - const ButtonRow = new MessageActionRow().addComponents( - new MessageButton({ - style: 'LINK', - label: 'Invite Me', - url: `https://discord.com/api/oauth2/authorize?client_id=${ - client.user!.id - }&permissions=2147483647&scope=bot%20applications.commands` - }) - ); - return await message.util.reply({ content: 'You can invite me here:', components: [ButtonRow] }); - } -} diff --git a/src/commands/info/links.ts b/src/commands/info/links.ts new file mode 100644 index 0000000..29152d9 --- /dev/null +++ b/src/commands/info/links.ts @@ -0,0 +1,45 @@ +import { BushCommand, BushMessage, BushSlashMessage } from '@lib'; +import { MessageActionRow, MessageButton } from 'discord.js'; +import packageDotJSON from '../../../package.json'; + +export default class LinksCommand extends BushCommand { + public constructor() { + super('links', { + aliases: ['links', 'invite', 'support'], + category: 'info', + description: { + content: 'Sends bot links', + usage: 'links', + examples: ['links'] + }, + ratelimit: 4, + cooldown: 4000, + clientPermissions: ['SEND_MESSAGES'], + slash: true + }); + } + + public override async exec(message: BushMessage | BushSlashMessage): Promise { + if (client.config.isDevelopment) return await message.util.reply(`${util.emojis.error} The dev bot cannot be invited.`); + const ButtonRow = new MessageActionRow().addComponents( + new MessageButton({ + style: 'LINK', + label: 'Invite Me', + url: `https://discord.com/api/oauth2/authorize?client_id=${ + client.user!.id + }&permissions=2147483647&scope=bot%20applications.commands` + }), + new MessageButton({ + style: 'LINK', + label: 'Support Server', + url: client.config.supportGuild.invite + }), + new MessageButton({ + style: 'LINK', + label: 'GitHub', + url: packageDotJSON.repository + }) + ); + return await message.util.reply({ content: '\u200B', components: [ButtonRow] }); + } +} diff --git a/src/commands/info/pronouns.ts b/src/commands/info/pronouns.ts index c7eac7f..96040c0 100644 --- a/src/commands/info/pronouns.ts +++ b/src/commands/info/pronouns.ts @@ -40,7 +40,7 @@ export default class PronounsCommand extends BushCommand { args: [ { id: 'user', - type: 'user', + customType: util.arg.union('user', 'bigint'), prompt: { start: 'Who would you like to view the pronouns of?', retry: '{error} Choose a valid user to view the pronouns of.', @@ -60,8 +60,16 @@ export default class PronounsCommand extends BushCommand { slash: true }); } - override async exec(message: BushMessage | BushSlashMessage, args: { user?: User }): Promise { - const user = args.user ?? message.author; + override async exec(message: BushMessage | BushSlashMessage, args: { user?: User | string }): Promise { + const user = + args?.user === undefined || args?.user === null + ? message.author + : typeof args.user === 'object' + ? args.user + : await client.users.fetch(`${args.user}`).catch(() => undefined); + + if (user === undefined) return message.util.reply(`${util.emojis.error} Invalid user.`); + const author = user.id === message.author.id; try { const apiRes: { pronouns: pronounsType } = await got diff --git a/src/commands/info/snowflakeInfo.ts b/src/commands/info/snowflakeInfo.ts index c4d71da..02c8d39 100644 --- a/src/commands/info/snowflakeInfo.ts +++ b/src/commands/info/snowflakeInfo.ts @@ -88,7 +88,7 @@ export default class SnowflakeInfoCommand extends BushCommand { } // Guild - else if (client.guilds.cache.has(snowflake)) { + if (client.guilds.cache.has(snowflake)) { const guild: Guild = client.guilds.cache.get(snowflake)!; const guildInfo = [ `**Name:** ${guild.name}`, @@ -101,8 +101,9 @@ export default class SnowflakeInfoCommand extends BushCommand { } // User - else if (client.users.cache.has(snowflake)) { - const user: User = client.users.cache.get(snowflake)!; + const fetchedUser = await client.users.fetch(`${snowflake}`).catch(() => undefined); + if (client.users.cache.has(snowflake) || fetchedUser) { + const user: User = (client.users.cache.get(snowflake) ?? fetchedUser)!; const userInfo = [`**Name:** <@${user.id}> (${user.tag})`]; if (user.avatar) snowflakeEmbed.setThumbnail(user.avatarURL({ size: 2048, dynamic: true })!); snowflakeEmbed.addField('» User Info', userInfo.join('\n')); @@ -110,7 +111,7 @@ export default class SnowflakeInfoCommand extends BushCommand { } // Emoji - else if (client.emojis.cache.has(snowflake)) { + if (client.emojis.cache.has(snowflake)) { const emoji: Emoji = client.emojis.cache.get(snowflake)!; const emojiInfo = [`**Name:** ${emoji.name}`, `**Animated:** ${emoji.animated}`]; if (emoji.url) snowflakeEmbed.setThumbnail(emoji.url); @@ -119,7 +120,7 @@ export default class SnowflakeInfoCommand extends BushCommand { } // Role - else if (message.guild && message.guild.roles.cache.has(snowflake)) { + if (message.guild && message.guild.roles.cache.has(snowflake)) { const role: Role = message.guild.roles.cache.get(snowflake)!; const roleInfo = [ `**Name:** <@&${role.id}> (${role.name})`, diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts index 9ec5552..7b8d7d8 100644 --- a/src/commands/info/userInfo.ts +++ b/src/commands/info/userInfo.ts @@ -1,7 +1,6 @@ import { BushCommand, BushMessage, BushSlashMessage, BushUser } from '@lib'; import { MessageEmbed } from 'discord.js'; -// TODO: Re-Implement Status Emojis // TODO: Add bot information export default class UserInfoCommand extends BushCommand { public constructor() { @@ -141,6 +140,24 @@ export default class UserInfoCommand extends BushCommand { presenceInfo.push(`**Activit${activitiesNames.length - 1 ? 'ies' : 'y'}:** ${util.oxford(activitiesNames, 'and', '')}`); if (customStatus && customStatus.length) presenceInfo.push(`**Custom Status:** ${customStatus}`); userEmbed.addField('» Presence', presenceInfo.join('\n')); + + enum statusEmojis { + online = '787550449435803658', + idle = '787550520956551218', + dnd = '787550487633330176', + offline = '787550565382750239', + invisible = '787550565382750239' + } + userEmbed.setFooter(user.tag, client.emojis.cache.get(statusEmojis[member?.presence.status])?.url ?? undefined); + } + + // roles + if (member?.roles.cache.size && member?.roles.cache.size - 1) { + const roles = member?.roles.cache + .filter((role) => role.name !== '@everyone') + .sort((role1, role2) => role2.position - role1.position) + .map((role) => `${role}`); + userEmbed.addField(`» Role${roles.length - 1 ? 's' : ''} [${roles.length}]`, roles.join(', ')); } // Important Perms diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index e32bf18..8a98998 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -90,6 +90,7 @@ export default class BanCommand extends BushCommand { force }: { user: User; reason?: { duration: number; contentWithoutTime: string }; days?: number; force: boolean } ): Promise { + if (!message.guild) return message.util.reply(`${util.emojis.error} This command cannot be used in dms.`); const member = message.guild!.members.cache.get(user.id) as BushGuildMember; const useForce = force && message.author.isOwner(); const canModerateResponse = util.moderationPermissionCheck(message.member!, member, 'ban', true, useForce); diff --git a/src/commands/utilities/activity.ts b/src/commands/utilities/activity.ts index 8bdfc21..c0ab2a2 100644 --- a/src/commands/utilities/activity.ts +++ b/src/commands/utilities/activity.ts @@ -1,4 +1,4 @@ -import { Message, VoiceChannel } from 'discord.js'; +import { DiscordAPIError, Message, VoiceChannel } from 'discord.js'; import { BushCommand, BushMessage, BushSlashMessage } from '../../lib'; const activityMap = { @@ -19,7 +19,7 @@ function map(phase: string) { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -const activityTypeCaster = (_message: Message, phrase: string) => { +const activityTypeCaster = (_message: Message | BushMessage | BushSlashMessage, phrase: string) => { if (!phrase) return null; const mappedPhrase = map(phrase); if (mappedPhrase) return mappedPhrase; @@ -51,7 +51,12 @@ export default class YouTubeCommand extends BushCommand { { id: 'activity', match: 'rest', - customType: activityTypeCaster + customType: activityTypeCaster, + prompt: { + start: 'What activity would you like to play?', + retry: + '{error} You must choose one of the following options: `yt`, `youtube`, `chess`, `park`, `poker`, `fish`, `fishing`, `fishington`, or `betrayal`.' + } } ], slash: true, @@ -85,29 +90,35 @@ export default class YouTubeCommand extends BushCommand { message: BushMessage | BushSlashMessage, args: { channel: VoiceChannel; activity: string } ): Promise { - if (!args.channel?.id || args.channel?.type != 'GUILD_VOICE') + const channel = typeof args.channel === 'string' ? message.guild?.channels.cache.get(args.channel) : args.channel; + if (!channel || channel.type !== 'GUILD_VOICE') return await message.util.reply(`${util.emojis.error} Choose a valid voice channel`); - let target_application_id: string; - if (message.util.isSlash) target_application_id = args.activity; - else target_application_id = target_application_id = args.activity; + const target_application_id = message.util.isSlash ? args.activity : activityTypeCaster(message, args.activity); - // @ts-ignore: jank typings - // prettier-ignore - const invite = await this.client.api.channels(args.channel.id) - .invites.post({ - data: { - validate: null, - max_age: 604800, - max_uses: 0, - target_type: 2, - target_application_id, - temporary: false - } - }) - .catch(() => false); - if (!invite || !invite.code) - return await message.util.reply(`${this.client.util.emojis.error} An error occurred while generating your invite.`); + let response: string; + const invite = await (client as any).api + .channels(channel.id) + .invites.post({ + data: { + validate: null, + max_age: 604800, + max_uses: 0, + target_type: 2, + target_application_id, + temporary: false + } + }) + .catch((e: Error | DiscordAPIError) => { + if ((e as DiscordAPIError).code === 50013) { + response = `${util.emojis.error} I am missing permissions to make an invite in that channel.`; + return; + } else response = `${util.emojis.error} An error occurred while generating your invite: ${e?.message ?? e}`; + }); + if (response! || !invite || !invite.code) + return await message.util.reply( + response! ?? `${util.emojis.error} An unknown error occurred while generating your invite.` + ); else return await message.util.send(`https://discord.gg/${invite.code}`); } } diff --git a/src/commands/utilities/steal.ts b/src/commands/utilities/steal.ts new file mode 100644 index 0000000..92abcb2 --- /dev/null +++ b/src/commands/utilities/steal.ts @@ -0,0 +1,59 @@ +import { BushCommand, BushMessage } from '@lib'; +import { Emoji } from 'discord.js'; + +export default class StealCommand extends BushCommand { + public constructor() { + super('steal', { + aliases: ['steal', 'copyemoji'], + category: 'utilities', + description: { + content: 'Steal an emoji from another server and add it to your own.', + usage: 'steal [--name name]', + examples: ['steal <:omegaclown:782630946435366942> --name ironm00n'] + }, + args: [ + { + id: 'emoji', + customType: util.arg.union('emoji', 'url'), + prompt: { + start: 'What emoji would you like to steal?', + retry: '{error} Pick a valid emoji.', + optional: true + } + }, + { id: 'name', match: 'option', flag: '--name', default: 'stolen_emoji' } + ], + slash: false, + channel: 'guild', + clientPermissions: ['SEND_MESSAGES', 'MANAGE_EMOJIS_AND_STICKERS'], + userPermissions: ['SEND_MESSAGES', 'MANAGE_EMOJIS_AND_STICKERS'] + }); + } + public override async exec(message: BushMessage, args: { emoji?: URL | Emoji; name: string }): Promise { + if ((!args || !args.emoji) && !message.attachments.size) + return await message.util.reply(`${util.emojis.error} You must provide an emoji to steal.`); + const image = + message.attachments.size && message.attachments.first()!.contentType?.includes('image/') + ? message.attachments.first()!.url + : args?.emoji instanceof Emoji + ? `https://cdn.discordapp.com/emojis/${args.emoji.id}` + : args?.emoji instanceof URL + ? args.emoji.href + : undefined; + + if (!image) return await message.util.reply(`${util.emojis.error} You must provide an emoji to steal.`); + + const creationSuccess = await message + .guild!.emojis.create(image, args.name, { + reason: `Stolen by ${message.author.tag} (${message.author.id})` + }) + .catch((e: Error) => e); + + if (!(creationSuccess instanceof Error)) + return await message.util.reply(`${util.emojis.success} You successfully stole ${creationSuccess}.`); + else + return await message.util.reply( + `${util.emojis.error} The was an error stealing that emoji \`${creationSuccess.message}\`.` + ); + } +} diff --git a/src/commands/utilities/whoHasRole.ts b/src/commands/utilities/whoHasRole.ts index a6c4665..e507036 100644 --- a/src/commands/utilities/whoHasRole.ts +++ b/src/commands/utilities/whoHasRole.ts @@ -33,7 +33,8 @@ export default class WhoHasRoleCommand extends BushCommand { ], channel: 'guild', clientPermissions: ['SEND_MESSAGES'], - userPermissions: ['SEND_MESSAGES'] + userPermissions: ['SEND_MESSAGES'], + typing: true }); } public override async exec(message: BushMessage | BushSlashMessage, args: { role: Role }): Promise { diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 88985e1..17f7d32 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -16,7 +16,15 @@ import { ModLogType } from '@lib'; import { exec } from 'child_process'; -import { Argument, ArgumentTypeCaster, ClientUtil, Flag, ParsedValuePredicate, TypeResolver } from 'discord-akairo'; +import { + Argument, + ArgumentTypeCaster, + ClientUtil, + Flag, + ParsedValuePredicate, + TypeResolver, + Util as AkairoUtil +} from 'discord-akairo'; import { APIMessage } from 'discord-api-types'; import { ButtonInteraction, @@ -34,7 +42,7 @@ import { Snowflake, TextChannel, User, - Util + Util as DiscordUtil } from 'discord.js'; import got from 'got'; import humanizeDuration from 'humanize-duration'; @@ -68,30 +76,6 @@ export interface uuidRes { created_at: string; } -interface bushColors { - default: '#1FD8F1'; - error: '#EF4947'; - warn: '#FEBA12'; - success: '#3BB681'; - info: '#3B78FF'; - red: '#ff0000'; - blue: '#0055ff'; - aqua: '#00bbff'; - purple: '#8400ff'; - blurple: '#5440cd'; - pink: '#ff00e6'; - green: '#00ff1e'; - darkGreen: '#008f11'; - gold: '#b59400'; - yellow: '#ffff00'; - white: '#ffffff'; - gray: '#a6a6a6'; - lightGray: '#cfcfcf'; - darkGray: '#7a7a7a'; - black: '#000000'; - orange: '#E86100'; -} - interface MojangProfile { username: string; uuid: string; @@ -499,9 +483,14 @@ export interface BushInspectOptions extends InspectOptions { } export class BushClientUtil extends ClientUtil { - /** The client of this ClientUtil */ + /** + * The client. + */ public declare readonly client: BushClient; - /** The hastebin urls used to post to hastebin, attempts to post in order */ + + /** + * The hastebin urls used to post to hastebin, attempts to post in order + */ #hasteURLs: string[] = [ 'https://hst.sh', 'https://hasteb.in', @@ -512,6 +501,10 @@ export class BushClientUtil extends ClientUtil { 'https://haste.unbelievaboat.com', 'https://haste.tyman.tech' ]; + + /** + * Emojis used for {@link BushClientUtil.buttonPaginate} + */ #paginateEmojis = { beginning: '853667381335162910', back: '853667410203770881', @@ -520,7 +513,9 @@ export class BushClientUtil extends ClientUtil { end: '853667514915225640' }; - /** A simple promise exec method */ + /** + * A simple promise exec method + */ #exec = promisify(exec); /** @@ -632,48 +627,19 @@ export class BushClientUtil extends ClientUtil { }, []); } - /** Commonly Used Colors */ - public colors: bushColors = { - default: '#1FD8F1', - error: '#EF4947', - warn: '#FEBA12', - success: '#3BB681', - info: '#3B78FF', - red: '#ff0000', - blue: '#0055ff', - aqua: '#00bbff', - purple: '#8400ff', - blurple: '#5440cd', - pink: '#ff00e6', - green: '#00ff1e', - darkGreen: '#008f11', - gold: '#b59400', - yellow: '#ffff00', - white: '#ffffff', - gray: '#a6a6a6', - lightGray: '#cfcfcf', - darkGray: '#7a7a7a', - black: '#000000', - orange: '#E86100' - }; + /** + * Commonly Used Colors + */ + get colors() { + return client.consts.colors; + } - /** Commonly Used Emojis */ - public emojis = { - success: '<:checkmark:837109864101707807>', - warn: '<:warn:848726900876247050>', - error: '<:error:837123021016924261>', - successFull: '<:checkmark_full:850118767576088646>', - warnFull: '<:warn_full:850118767391539312>', - errorFull: '<:error_full:850118767295201350>', - mad: '<:mad:783046135392239626>', - join: '<:join:850198029809614858>', - leave: '<:leave:850198048205307919>', - loading: '', - offlineCircle: '<:offline:787550565382750239>', - dndCircle: '<:dnd:787550487633330176>', - idleCircle: '<:idle:787550520956551218>', - onlineCircle: '<:online:787550449435803658>' - }; + /** + * Commonly Used Emojis + */ + get emojis() { + return client.consts.emojis; + } /** * A simple utility to create and embed with the needed style for the bot @@ -698,7 +664,9 @@ export class BushClientUtil extends ClientUtil { return apiRes.uuid.replace(/-/g, ''); } - /** Paginates an array of embeds using buttons. */ + /** + * Paginates an array of embeds using buttons. + */ public async buttonPaginate( message: BushMessage | BushSlashMessage, embeds: MessageEmbed[], @@ -880,8 +848,34 @@ export class BushClientUtil extends ClientUtil { return code3; } - public inspect(code: any, options: BushInspectOptions): string { - return inspect(code, options); + public inspect(code: any, options?: BushInspectOptions): string { + const { + showHidden: _showHidden = false, + depth: _depth = 2, + colors: _colors = false, + customInspect: _customInspect = true, + showProxy: _showProxy = false, + maxArrayLength: _maxArrayLength = Infinity, + maxStringLength: _maxStringLength = Infinity, + breakLength: _breakLength = 80, + compact: _compact = 3, + sorted: _sorted = false, + getters: _getters = true + } = options ?? {}; + const optionsWithDefaults: BushInspectOptions = { + showHidden: _showHidden, + depth: _depth, + colors: _colors, + customInspect: _customInspect, + showProxy: _showProxy, + maxArrayLength: _maxArrayLength, + maxStringLength: _maxStringLength, + breakLength: _breakLength, + compact: _compact, + sorted: _sorted, + getters: _getters + }; + return inspect(code, optionsWithDefaults); } #mapCredential(old: string): string { @@ -911,45 +905,31 @@ export class BushClientUtil extends ClientUtil { return text; } + /** + * Takes an any value, inspects it, redacts credentials and puts it in a codeblock + * (and uploads to hast if the content is too long) + */ public async inspectCleanRedactCodeblock( input: any, language?: CodeBlockLang, inspectOptions?: BushInspectOptions, length = 1024 ) { - const { - showHidden: _showHidden = false, - depth: _depth = 2, - colors: _colors = false, - customInspect: _customInspect = true, - showProxy: _showProxy = false, - maxArrayLength: _maxArrayLength = Infinity, - maxStringLength: _maxStringLength = Infinity, - breakLength: _breakLength = 80, - compact: _compact = 3, - sorted: _sorted = false, - getters: _getters = true - } = inspectOptions ?? {}; - const inspectOptionsWithDefaults: BushInspectOptions = { - showHidden: _showHidden, - depth: _depth, - colors: _colors, - customInspect: _customInspect, - showProxy: _showProxy, - maxArrayLength: _maxArrayLength, - maxStringLength: _maxStringLength, - breakLength: _breakLength, - compact: _compact, - sorted: _sorted, - getters: _getters - }; - input = - typeof input !== 'string' && inspectOptionsWithDefaults !== undefined - ? this.inspect(input, inspectOptionsWithDefaults) - : input; - input = Util.cleanCodeBlockContent(input); + input = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; + input = this.discord.cleanCodeBlockContent(input); input = this.redact(input); - return client.util.codeblock(input, length, language); + return this.codeblock(input, length, language); + } + + public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions) { + input = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; + input = this.redact(input); + return this.haste(input); + } + + public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions) { + input = typeof input !== 'string' ? this.inspect(input, inspectOptions ?? undefined) : input; + return this.redact(input); } public async slashRespond( @@ -973,7 +953,8 @@ export class BushClientUtil extends ClientUtil { } /** - * Gets a a configured channel as a TextChannel */ + * Gets a a configured channel as a TextChannel + */ public async getConfigChannel(channel: keyof typeof client['config']['channels']): Promise { return (await client.channels.fetch(client.config.channels[channel])) as unknown as TextChannel; } @@ -1256,122 +1237,124 @@ export class BushClientUtil extends ClientUtil { return string.charAt(0)?.toUpperCase() + string.slice(1); } - public arg = new (class Arg { - /** - * Casts a phrase to this argument's type. - * @param type - The type to cast to. - * @param resolver - The type resolver. - * @param message - Message that called the command. - * @param phrase - Phrase to process. - */ - public cast(type: BushArgumentType, resolver: TypeResolver, message: Message, phrase: string): Promise { - return Argument.cast(type, resolver, message, phrase); - } + get arg() { + return class Arg { + /** + * Casts a phrase to this argument's type. + * @param type - The type to cast to. + * @param resolver - The type resolver. + * @param message - Message that called the command. + * @param phrase - Phrase to process. + */ + public static cast(type: BushArgumentType, resolver: TypeResolver, message: Message, phrase: string): Promise { + return Argument.cast(type, resolver, message, phrase); + } - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the entire composition fails. - * @param types - Types to use. - */ - public compose(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.compose(...types); - } + /** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the entire composition fails. + * @param types - Types to use. + */ + public static compose(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.compose(...types); + } - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the composition still continues with the failure passed on. - * @param types - Types to use. - */ - public composeWithFailure(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.composeWithFailure(...types); - } + /** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the composition still continues with the failure passed on. + * @param types - Types to use. + */ + public static composeWithFailure(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.composeWithFailure(...types); + } - /** - * Checks if something is null, undefined, or a fail flag. - * @param value - Value to check. - */ - public isFailure(value: any): value is null | undefined | (Flag & { value: any }) { - return Argument.isFailure(value); - } + /** + * Checks if something is null, undefined, or a fail flag. + * @param value - Value to check. + */ + public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) { + return Argument.isFailure(value); + } - /** - * Creates a type from multiple types (product type). - * Only inputs where each type resolves with a non-void value are valid. - * @param types - Types to use. - */ - public product(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.product(...types); - } + /** + * Creates a type from multiple types (product type). + * Only inputs where each type resolves with a non-void value are valid. + * @param types - Types to use. + */ + public static product(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.product(...types); + } - /** - * Creates a type where the parsed value must be within a range. - * @param type - The type to use. - * @param min - Minimum value. - * @param max - Maximum value. - * @param inclusive - Whether or not to be inclusive on the upper bound. - */ - public range(type: BushArgumentType, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster { - return Argument.range(type, min, max, inclusive); - } + /** + * Creates a type where the parsed value must be within a range. + * @param type - The type to use. + * @param min - Minimum value. + * @param max - Maximum value. + * @param inclusive - Whether or not to be inclusive on the upper bound. + */ + public static range(type: BushArgumentType, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster { + return Argument.range(type, min, max, inclusive); + } - /** - * Creates a type that parses as normal but also tags it with some data. - * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public tagged(type: BushArgumentType, tag?: any): ArgumentTypeCaster { - return Argument.tagged(type, tag); - } + /** + * Creates a type that parses as normal but also tags it with some data. + * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ + public static tagged(type: BushArgumentType, tag?: any): ArgumentTypeCaster { + return Argument.tagged(type, tag); + } - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * Each type will also be tagged using `tagged` with themselves. - * @param types - Types to use. - */ - public taggedUnion(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.taggedUnion(...types); - } + /** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * Each type will also be tagged using `tagged` with themselves. + * @param types - Types to use. + */ + public static taggedUnion(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.taggedUnion(...types); + } - /** - * Creates a type that parses as normal but also tags it with some data and carries the original input. - * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public taggedWithInput(type: BushArgumentType, tag?: any): ArgumentTypeCaster { - return Argument.taggedWithInput(type, tag); - } + /** + * Creates a type that parses as normal but also tags it with some data and carries the original input. + * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ + public static taggedWithInput(type: BushArgumentType, tag?: any): ArgumentTypeCaster { + return Argument.taggedWithInput(type, tag); + } - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * @param types - Types to use. - */ - public union(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.union(...types); - } + /** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * @param types - Types to use. + */ + public static union(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.union(...types); + } - /** - * Creates a type with extra validation. - * If the predicate is not true, the value is considered invalid. - * @param type - The type to use. - * @param predicate - The predicate function. - */ - public validate(type: BushArgumentType, predicate: ParsedValuePredicate): ArgumentTypeCaster { - return Argument.validate(type, predicate); - } + /** + * Creates a type with extra validation. + * If the predicate is not true, the value is considered invalid. + * @param type - The type to use. + * @param predicate - The predicate function. + */ + public static validate(type: BushArgumentType, predicate: ParsedValuePredicate): ArgumentTypeCaster { + return Argument.validate(type, predicate); + } - /** - * Creates a type that parses as normal but also carries the original input. - * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - */ - public withInput(type: BushArgumentType): ArgumentTypeCaster { - return Argument.withInput(type); - } - })(); + /** + * Creates a type that parses as normal but also carries the original input. + * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + */ + public static withInput(type: BushArgumentType): ArgumentTypeCaster { + return Argument.withInput(type); + } + }; + } /** * Wait an amount in seconds. @@ -1406,4 +1389,18 @@ export class BushClientUtil extends ClientUtil { // return props.join('\n'); // } + + /** + * Discord.js's Util class + */ + get discord() { + return DiscordUtil; + } + + /** + * discord-akairo's Util class + */ + get akairo() { + return AkairoUtil; + } } diff --git a/src/lib/extensions/discord.js/BushClientUser.d.ts b/src/lib/extensions/discord.js/BushClientUser.d.ts index 035c6d9..1a6cea4 100644 --- a/src/lib/extensions/discord.js/BushClientUser.d.ts +++ b/src/lib/extensions/discord.js/BushClientUser.d.ts @@ -2,8 +2,8 @@ import { ActivityOptions, Base64Resolvable, BufferResolvable, + ClientPresence, ClientUserEditData, - Presence as BushPresence, PresenceData, PresenceStatusData } from 'discord.js'; @@ -11,13 +11,14 @@ import { BushUser } from './BushUser'; export class BushClientUser extends BushUser { public mfaEnabled: boolean; + public readonly presence: ClientPresence; public verified: boolean; public edit(data: ClientUserEditData): Promise; - public setActivity(options?: ActivityOptions): BushPresence; - public setActivity(name: string, options?: ActivityOptions): BushPresence; - public setAFK(afk: boolean, shardId?: number | number[]): BushPresence; + public setActivity(options?: ActivityOptions): ClientPresence; + public setActivity(name: string, options?: ActivityOptions): ClientPresence; + public setAFK(afk: boolean, shardId?: number | number[]): ClientPresence; public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise; - public setPresence(data: PresenceData): BushPresence; - public setStatus(status: PresenceStatusData, shardId?: number | number[]): BushPresence; + public setPresence(data: PresenceData): ClientPresence; + public setStatus(status: PresenceStatusData, shardId?: number | number[]): ClientPresence; public setUsername(username: string): Promise; } diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index e58380b..68393c4 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -1,4 +1,68 @@ +interface bushColors { + default: '#1FD8F1'; + error: '#EF4947'; + warn: '#FEBA12'; + success: '#3BB681'; + info: '#3B78FF'; + red: '#ff0000'; + blue: '#0055ff'; + aqua: '#00bbff'; + purple: '#8400ff'; + blurple: '#5440cd'; + pink: '#ff00e6'; + green: '#00ff1e'; + darkGreen: '#008f11'; + gold: '#b59400'; + yellow: '#ffff00'; + white: '#ffffff'; + gray: '#a6a6a6'; + lightGray: '#cfcfcf'; + darkGray: '#7a7a7a'; + black: '#000000'; + orange: '#E86100'; +} export class BushConstants { + public static emojis = { + success: '<:checkmark:837109864101707807>', + warn: '<:warn:848726900876247050>', + error: '<:error:837123021016924261>', + successFull: '<:checkmark_full:850118767576088646>', + warnFull: '<:warn_full:850118767391539312>', + errorFull: '<:error_full:850118767295201350>', + mad: '<:mad:783046135392239626>', + join: '<:join:850198029809614858>', + leave: '<:leave:850198048205307919>', + loading: '', + offlineCircle: '<:offline:787550565382750239>', + dndCircle: '<:dnd:787550487633330176>', + idleCircle: '<:idle:787550520956551218>', + onlineCircle: '<:online:787550449435803658>' + }; + + public static colors: bushColors = { + default: '#1FD8F1', + error: '#EF4947', + warn: '#FEBA12', + success: '#3BB681', + info: '#3B78FF', + red: '#ff0000', + blue: '#0055ff', + aqua: '#00bbff', + purple: '#8400ff', + blurple: '#5440cd', + pink: '#ff00e6', + green: '#00ff1e', + darkGreen: '#008f11', + gold: '#b59400', + yellow: '#ffff00', + white: '#ffffff', + gray: '#a6a6a6', + lightGray: '#cfcfcf', + darkGray: '#7a7a7a', + black: '#000000', + orange: '#E86100' + }; + // Somewhat stolen from @Mzato0001 public static TimeUnits: { [key: string]: { match: RegExp; value: number } } = { years: { diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts index 60817b9..e716e68 100644 --- a/src/lib/utils/BushLogger.ts +++ b/src/lib/utils/BushLogger.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import chalk from 'chalk'; -import { MessageEmbed, Util } from 'discord.js'; +import { Message, MessageEmbed, Util } from 'discord.js'; import { inspect } from 'util'; import { BushSendMessageType } from '../extensions/discord-akairo/BushClient'; @@ -69,9 +69,9 @@ export class BushLogger { } /** Sends a message to the error channel */ - public static async channelError(message: BushSendMessageType): Promise { + public static async channelError(message: BushSendMessageType): Promise { const channel = await util.getConfigChannel('error'); - await channel.send(message).catch(() => {}); + return await channel.send(message); } /** @@ -177,6 +177,7 @@ export class BushLogger { .setColor(util.colors.error) .setTimestamp(); await this.channelError({ embeds: [embed] }); + return; } /** diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index d4f68f2..85cd465 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -1,6 +1,6 @@ import { BushCommandHandlerEvents, BushListener } from '@lib'; -import { GuildTextBasedChannels } from 'discord-akairo'; -import { DMChannel, MessageEmbed } from 'discord.js'; +import { AkairoMessage, Command, GuildTextBasedChannels } from 'discord-akairo'; +import { DMChannel, Message, MessageEmbed } from 'discord.js'; export default class CommandErrorListener extends BushListener { public constructor() { @@ -15,70 +15,106 @@ export default class CommandErrorListener extends BushListener { } public static async handleError( - ...[error, message, command]: BushCommandHandlerEvents['error'] | BushCommandHandlerEvents['slashError'] + ...[error, message, _command]: BushCommandHandlerEvents['error'] | BushCommandHandlerEvents['slashError'] ): Promise { const isSlash = message.util!.isSlash; - - const errorNo = Math.floor(Math.random() * 6969696969) + 69; // hehe funny number + const errorNum = Math.floor(Math.random() * 6969696969) + 69; // hehe funny number const channel = message.channel!.type === 'DM' ? (message.channel as DMChannel)!.recipient.tag : (message.channel as GuildTextBasedChannels)!.name; - const errorEmbed: MessageEmbed = new MessageEmbed() - .setTitle(`${isSlash ? 'Slash ' : ''}Error # \`${errorNo}\`: An error occurred`) - .addField('Error', await util.inspectCleanRedactCodeblock(error?.stack ?? error, 'js', undefined)) - .setColor(util.colors.error) - .setTimestamp(); - const description = [ - `**User:** ${message.author} (${message.author.tag})`, - `**Command:** ${command ?? message?.util?.parsed?.command ?? 'N/A'}`, - `**Channel:** ${channel} (${message.channel?.id})`, - `**Message:** [link](${message.url})` - ]; - if ('code' in error) description.push(`**Error Code:** \`${(error as any).code}\``); - if (message?.util?.parsed?.content) description.push(`**Command Content:** ${message.util.parsed.content}`); - errorEmbed.setDescription(description.join('\n')); - await client.logger.channelError({ embeds: [errorEmbed] }); - const heading = `${isSlash ? 'Slash' : 'Command'}Error`; - if (message) { - if (!client.config.owners.includes(message.author.id)) { - const errorUserEmbed: MessageEmbed = new MessageEmbed() - .setTitle('A Command Error Occurred') - .setColor(util.colors.error) - .setTimestamp(); - if (!command) - errorUserEmbed.setDescription(`Oh no! An error occurred. Please give the developers code \`${errorNo}\`.`); - else - errorUserEmbed.setDescription( - `Oh no! While running the ${isSlash ? 'slash ' : ''}command \`${ - command.id - }\`, an error occurred. Please give the developers code \`${errorNo}\`.` - ); - (await message.util?.send({ embeds: [errorUserEmbed] }).catch((e) => { - void client.console.warn(heading, `Failed to send user error embed in <<${channel}>>:\n` + e?.stack || e); - })) ?? client.console.error(heading, `Failed to send user error embed.` + error?.stack || error, false); - } else { - const errorDevEmbed = new MessageEmbed() - .setTitle(`A Command Error Occurred ${'code' in error ? `\`${(error as any).code}\`` : ''}`) - .setColor(util.colors.error) - .setTimestamp() - .setDescription(await util.inspectCleanRedactCodeblock(error?.stack ?? error, 'js', undefined, 4096)); - (await message.util?.send({ embeds: [errorDevEmbed] }).catch((e) => { - const channel = message.channel - ? message.channel.type === 'DM' - ? message.channel.recipient.tag - : message.channel.name - : 'unknown'; - void client.console.warn(heading, `Failed to send owner error stack in <<${channel}>>.` + e?.stack || e); - })) ?? client.console.error(heading, `Failed to send owner error stack.` + error?.stack || error, false); - } - } + const command = _command ?? message.util?.parsed?.command; + void client.console.error( - heading, + `${isSlash ? 'Slash' : 'Command'}Error`, `an error occurred with the <<${command}>> ${isSlash ? 'slash ' : ''}command in <<${channel}>> triggered by <<${ message?.author?.tag }>>:\n` + error?.stack || error, false ); + + const options = { message, error, isSlash, errorNum, command, channel }; + + const errorEmbed = await CommandErrorListener.generateErrorEmbed({ + ...options, + type: 'command-log' + }); + + void client.logger.channelError({ embeds: [errorEmbed] }); + + if (message) { + if (!client.config.owners.includes(message.author.id)) { + const errorUserEmbed = await CommandErrorListener.generateErrorEmbed({ + ...options, + type: 'command-user' + }); + void message.util?.send({ embeds: [errorUserEmbed] }).catch(() => null); + } else { + const errorDevEmbed = await CommandErrorListener.generateErrorEmbed({ + ...options, + type: 'command-dev' + }); + void message.util?.send({ embeds: [errorDevEmbed] }).catch(() => null); + } + } + } + + public static async generateErrorEmbed( + options: + | { + message: Message | AkairoMessage; + error: Error | any; + isSlash: boolean; + type: 'command-log' | 'command-dev' | 'command-user'; + errorNum: number; + command?: Command; + channel?: string; + } + | { error: Error | any; type: 'uncaughtException' | 'unhandledRejection' } + ): Promise { + const embed = new MessageEmbed().setColor(util.colors.error).setTimestamp(); + if (options.type === 'command-user') { + return embed + .setTitle('An Error Occurred') + .setDescription( + `Oh no! ${ + options.command ? `While running the ${options.isSlash ? 'slash ' : ''}command \`${options.command.id}\`, a` : 'A' + }n error occurred. Please give the developers code \`${options.errorNum}\`.` + ); + } + const description = new Array(); + + if (options.type === 'command-log') { + description.push( + `**User:** ${options.message.author} (${options.message.author.tag})`, + `**Command:** ${options.command ?? 'N/A'}`, + `**Channel:** <#${options.message.channel?.id}> (${options.channel})`, + `**Message:** [link](${options.message.url})` + ); + if (options.message?.util?.parsed?.content) + description.push(`**Command Content:** ${options.message.util.parsed.content}`); + } + for (const element in options.error) { + if (['stack', 'name', 'message'].includes(element)) continue; + else { + description.push( + `**Error ${util.capitalizeFirstLetter(element)}:** ${ + typeof (options.error as any)[element] === 'object' + ? `[haste](${await util.inspectCleanRedactHaste((options.error as any)[element])})` + : '`' + util.discord.escapeInlineCode(util.inspectAndRedact((options.error as any)[element])) + '`' + }` + ); + } + } + + embed + .addField('Stack Trace', await util.inspectCleanRedactCodeblock(options.error?.stack ?? options.error, 'js')) + .setDescription(description.join('\n')); + + if (options.type === 'command-dev' || options.type === 'command-log') + embed.setTitle(`${options.isSlash ? 'Slash ' : ''}CommandError #\`${options.errorNum}\``); + else if (options.type === 'uncaughtException') embed.setTitle('Uncaught Exception'); + else if (options.type === 'unhandledRejection') embed.setTitle('Unhandled Promise Rejection'); + return embed; } } diff --git a/src/listeners/other/promiseRejection.ts b/src/listeners/other/promiseRejection.ts index 24493f7..ea6f9d1 100644 --- a/src/listeners/other/promiseRejection.ts +++ b/src/listeners/other/promiseRejection.ts @@ -1,4 +1,5 @@ import { BushListener } from '@lib'; +import CommandErrorListener from '../commands/commandError'; export default class PromiseRejectionListener extends BushListener { public constructor() { @@ -12,14 +13,7 @@ export default class PromiseRejectionListener extends BushListener { // eslint-disable-next-line @typescript-eslint/no-base-to-string void client.console.error('PromiseRejection', `An unhanded promise rejection occurred:\n${error?.stack ?? error}`, false); void client.console.channelError({ - embeds: [ - { - title: 'Unhandled promise rejection', - // eslint-disable-next-line @typescript-eslint/no-base-to-string - fields: [{ name: 'error', value: await util.codeblock(`${error?.stack ?? error}`, 1024, 'js') }], - color: util.colors.error - } - ] + embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error })] }); } } diff --git a/src/listeners/other/uncaughtException.ts b/src/listeners/other/uncaughtException.ts index 5f4bd46..47db37f 100644 --- a/src/listeners/other/uncaughtException.ts +++ b/src/listeners/other/uncaughtException.ts @@ -1,4 +1,5 @@ import { BushListener } from '@lib'; +import CommandErrorListener from '../commands/commandError'; export default class UncaughtExceptionListener extends BushListener { public constructor() { @@ -12,14 +13,7 @@ export default class UncaughtExceptionListener extends BushListener { // eslint-disable-next-line @typescript-eslint/no-base-to-string void client.console.error('uncaughtException', `An uncaught exception occurred:\n${error?.stack ?? error}`, false); void client.console.channelError({ - embeds: [ - { - title: 'An uncaught exception occurred', - // eslint-disable-next-line @typescript-eslint/no-base-to-string - fields: [{ name: 'error', value: await util.codeblock(`${error?.stack ?? error}`, 1024, 'js') }], - color: util.colors.error - } - ] + embeds: [await CommandErrorListener.generateErrorEmbed({ type: 'uncaughtException', error: error })] }); } } -- cgit