aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/info/links.ts (renamed from src/commands/info/invite.ts)25
-rw-r--r--src/commands/info/pronouns.ts14
-rw-r--r--src/commands/info/snowflakeInfo.ts11
-rw-r--r--src/commands/info/userInfo.ts19
-rw-r--r--src/commands/moderation/ban.ts1
-rw-r--r--src/commands/utilities/activity.ts57
-rw-r--r--src/commands/utilities/steal.ts59
-rw-r--r--src/commands/utilities/whoHasRole.ts3
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts419
-rw-r--r--src/lib/extensions/discord.js/BushClientUser.d.ts13
-rw-r--r--src/lib/utils/BushConstants.ts64
-rw-r--r--src/lib/utils/BushLogger.ts7
-rw-r--r--src/listeners/commands/commandError.ts146
-rw-r--r--src/listeners/other/promiseRejection.ts10
-rw-r--r--src/listeners/other/uncaughtException.ts10
15 files changed, 527 insertions, 331 deletions
diff --git a/src/commands/info/invite.ts b/src/commands/info/links.ts
index 615e767..29152d9 100644
--- a/src/commands/info/invite.ts
+++ b/src/commands/info/links.ts
@@ -1,15 +1,16 @@
import { BushCommand, BushMessage, BushSlashMessage } from '@lib';
import { MessageActionRow, MessageButton } from 'discord.js';
+import packageDotJSON from '../../../package.json';
-export default class InviteCommand extends BushCommand {
+export default class LinksCommand extends BushCommand {
public constructor() {
- super('invite', {
- aliases: ['invite'],
+ super('links', {
+ aliases: ['links', 'invite', 'support'],
category: 'info',
description: {
- content: 'Sends the bot invite link.',
- usage: 'invite',
- examples: ['invite']
+ content: 'Sends bot links',
+ usage: 'links',
+ examples: ['links']
},
ratelimit: 4,
cooldown: 4000,
@@ -27,8 +28,18 @@ export default class InviteCommand extends BushCommand {
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: 'You can invite me here:', components: [ButtonRow] });
+ 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<unknown> {
- const user = args.user ?? message.author;
+ override async exec(message: BushMessage | BushSlashMessage, args: { user?: User | string }): Promise<unknown> {
+ 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<unknown> {
+ 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<unknown> {
- 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 <emoji/url> [--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<unknown> {
+ 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<unknown> {
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: '<a:Loading:853419254619963392>',
- 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<TextChannel> {
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<any> {
- 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<any> {
+ 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<this>;
- 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<this>;
- 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<this>;
}
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: '<a:Loading:853419254619963392>',
+ 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<void> {
+ public static async channelError(message: BushSendMessageType): Promise<Message> {
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<void> {
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<MessageEmbed> {
+ 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<string>();
+
+ 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 })]
});
}
}