aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/commands/config/prefix.ts (renamed from src/commands/server-config/prefix.ts)4
-rw-r--r--src/commands/dev/eval.ts144
-rw-r--r--src/commands/dev/reload.ts4
-rw-r--r--src/commands/dev/setLevel.ts2
-rw-r--r--src/commands/info/help.ts2
-rw-r--r--src/commands/info/ping.ts2
-rw-r--r--src/commands/info/pronouns.ts2
-rw-r--r--src/commands/moderation/ban.ts12
-rw-r--r--src/commands/moderation/kick.ts10
-rw-r--r--src/commands/moderation/role.ts22
-rw-r--r--src/commands/moulberry-bush/capePerms.ts13
-rw-r--r--src/commands/moulberry-bush/giveawayPing.ts7
-rw-r--r--src/commands/moulberry-bush/rule.ts8
-rw-r--r--src/config/example-options.ts4
-rw-r--r--src/lib/extensions/BushClient.ts78
-rw-r--r--src/lib/extensions/BushClientUtil.ts879
-rw-r--r--src/lib/extensions/BushCommand.ts35
-rw-r--r--src/lib/extensions/BushCommandHandler.ts274
-rw-r--r--src/lib/extensions/BushInhibitor.ts2
-rw-r--r--src/lib/extensions/BushInhinitorHandler.ts6
-rw-r--r--src/lib/extensions/BushListener.ts2
-rw-r--r--src/lib/extensions/BushTask.ts2
-rw-r--r--src/lib/extensions/BushTaskHandler.ts1
-rw-r--r--src/lib/extensions/Util.ts598
-rw-r--r--src/lib/utils/BushCache.ts5
-rw-r--r--src/lib/utils/BushLogger.ts191
-rw-r--r--src/lib/utils/Console.ts115
-rw-r--r--src/lib/utils/Logger.ts43
-rw-r--r--src/listeners/client/ready.ts21
-rw-r--r--src/listeners/commands/commandBlocked.ts8
-rw-r--r--src/listeners/commands/commandError.ts71
-rw-r--r--src/listeners/commands/commandStarted.ts13
-rw-r--r--src/listeners/message/level.ts3
-rw-r--r--src/listeners/other/consoleListener.ts50
-rw-r--r--src/listeners/other/promiseRejection.ts23
-rw-r--r--src/tasks/unban.ts5
36 files changed, 1735 insertions, 926 deletions
diff --git a/src/commands/server-config/prefix.ts b/src/commands/config/prefix.ts
index 9cdc331..c20cfa5 100644
--- a/src/commands/server-config/prefix.ts
+++ b/src/commands/config/prefix.ts
@@ -1,15 +1,15 @@
import { ApplicationCommandOptionType } from 'discord-api-types';
import { Guild as DiscordGuild, Message } from 'discord.js';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
import { Guild } from '../../lib/models';
export default class PrefixCommand extends BushCommand {
constructor() {
super('prefix', {
aliases: ['prefix'],
- category: 'server config',
+ category: 'config',
args: [
{
id: 'prefix'
diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts
index 2f1d45d..8bf88ff 100644
--- a/src/commands/dev/eval.ts
+++ b/src/commands/dev/eval.ts
@@ -1,16 +1,20 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { exec } from 'child_process';
import { Constants } from 'discord-akairo';
-import { Message, MessageEmbed, MessageEmbedOptions, Util } from 'discord.js';
+import { CommandInteraction, MessageEmbed, MessageEmbedOptions, Util } from 'discord.js';
import { transpile } from 'typescript';
import { inspect, promisify } from 'util';
import { BushCommand } from '../../lib/extensions/BushCommand';
+import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
+import { BushMessage } from '../../lib/extensions/BushMessage';
const clean = (text) => {
if (typeof text === 'string') {
return (text = Util.cleanCodeBlockContent(text));
} else return text;
};
+const sh = promisify(exec);
+
export default class EvalCommand extends BushCommand {
public constructor() {
super('eval', {
@@ -23,7 +27,7 @@ export default class EvalCommand extends BushCommand {
},
args: [
{
- id: 'selDepth',
+ id: 'sel_depth',
match: Constants.ArgumentMatches.OPTION,
type: Constants.ArgumentTypes.NUMBER,
flag: '--depth',
@@ -35,7 +39,7 @@ export default class EvalCommand extends BushCommand {
flag: '--sudo'
},
{
- id: 'deleteMSG',
+ id: 'delete_msg',
match: Constants.ArgumentMatches.FLAG,
flag: '--delete'
},
@@ -55,7 +59,7 @@ export default class EvalCommand extends BushCommand {
flag: '--hidden'
},
{
- id: 'showProto',
+ id: 'show_proto',
match: Constants.ArgumentMatches.FLAG,
flag: '--proto'
},
@@ -70,62 +74,99 @@ export default class EvalCommand extends BushCommand {
}
],
ownerOnly: true,
- clientPermissions: ['EMBED_LINKS']
+ slash: true,
+ slashOptions: [
+ {
+ name: 'code',
+ description: 'The code you would like to evaluate.',
+ type: 'STRING',
+ required: true
+ },
+ {
+ name: 'sel_depth',
+ description: 'How deep to display the output.',
+ type: 'INTEGER',
+ required: false
+ },
+ {
+ name: 'sudo',
+ description: 'Whether or not to override checks.',
+ type: 'BOOLEAN',
+ required: false
+ },
+ {
+ name: 'silent',
+ description: 'Whether or not to make the response silent',
+ type: 'BOOLEAN',
+ required: false
+ },
+ {
+ name: 'typescript',
+ description: 'Whether or not to compile the code from typescript.',
+ type: 'BOOLEAN',
+ required: false
+ },
+ {
+ name: 'hidden',
+ description: 'Whether or not to show hidden items.',
+ type: 'BOOLEAN',
+ required: false
+ },
+ {
+ name: 'show_proto',
+ description: 'Show prototype.',
+ type: 'BOOLEAN',
+ required: false
+ }
+ ]
});
}
- private redactCredentials(old: string) {
- const mapping = {
- ['token']: 'Token',
- ['devToken']: 'Dev Token',
- ['MongoDB']: 'MongoDB URI',
- ['hypixelApiKey']: 'Hypixel Api Key',
- ['webhookID']: 'Webhook ID',
- ['webhookToken']: 'Webhook Token'
- };
- return mapping[old] || old;
- }
-
public async exec(
- message: Message,
- {
- selDepth,
- code: codeArg,
- sudo,
- silent,
- deleteMSG,
- typescript,
- hidden,
- showProto
- }: {
- selDepth: number;
+ message: BushMessage | BushInteractionMessage,
+ args: {
+ sel_depth: number;
code: string;
sudo: boolean;
silent: boolean;
deleteMSG: boolean;
typescript: boolean;
hidden: boolean;
- showProto: boolean;
+ show_proto: boolean;
}
): Promise<unknown> {
if (!this.client.config.owners.includes(message.author.id))
- return await message.channel.send(`${this.client.util.emojis.error} Only my developers can run this command.`);
+ return await message.util.reply(`${this.client.util.emojis.error} Only my developers can run this command.`);
+ if (message.util.isSlash) {
+ await (message as BushInteractionMessage).interaction.defer({ ephemeral: args.silent });
+ }
+
const code: { js?: string | null; ts?: string | null; lang?: 'js' | 'ts' } = {};
- codeArg = codeArg.replace(/[“”]/g, '"');
- codeArg = codeArg.replace(/```/g, '');
- if (typescript) {
- code.ts = codeArg;
- code.js = transpile(codeArg);
+ args.code = args.code.replace(/[“”]/g, '"');
+ args.code = args.code.replace(/```/g, '');
+ if (args.typescript) {
+ code.ts = args.code;
+ code.js = transpile(args.code);
code.lang = 'ts';
} else {
code.ts = null;
- code.js = codeArg;
+ code.js = args.code;
code.lang = 'js';
}
const embed: MessageEmbed = new MessageEmbed();
const bad_phrases: string[] = ['delete', 'destroy'];
- if (bad_phrases.some((p) => code[code.lang].includes(p)) && !sudo) {
+
+ function ae(old: string) {
+ const mapping = {
+ ['token']: 'Token',
+ ['devToken']: 'Dev Token',
+ ['hypixelApiKey']: 'Hypixel Api Key'
+ };
+ return mapping[old] || old;
+ }
+
+ if (bad_phrases.some((p) => code[code.lang].includes(p)) && !args.sudo) {
return await message.util.send(`${this.client.util.emojis.error} This eval was blocked by smooth brain protection™.`);
}
const embeds: (MessageEmbed | MessageEmbedOptions)[] = [new MessageEmbed()];
@@ -140,10 +181,7 @@ export default class EvalCommand extends BushCommand {
channel = message.channel,
config = this.client.config,
members = message.guild.members,
- roles = message.guild.roles,
- sh = promisify(exec),
- models = this.client.db.models,
- got = require('got'); // eslint-disable-line @typescript-eslint/no-var-requires
+ roles = message.guild.roles;
if (code[code.lang].replace(/ /g, '').includes('9+10' || '10+9')) {
output = 21;
} else {
@@ -151,15 +189,15 @@ export default class EvalCommand extends BushCommand {
output = await output;
}
let proto, outputProto;
- if (showProto) {
+ if (args.show_proto) {
proto = Object.getPrototypeOf(output);
outputProto = clean(inspect(proto, { depth: 1, getters: true, showHidden: true }));
}
if (typeof output !== 'string')
- output = inspect(output, { depth: selDepth, showHidden: hidden, getters: true, showProxy: true });
+ output = inspect(output, { depth: args.sel_depth || 0, showHidden: args.hidden, getters: true, showProxy: true });
for (const credentialName in this.client.config.credentials) {
const credential = this.client.config.credentials[credentialName];
- const newCredential = this.redactCredentials(credentialName);
+ const newCredential = ae(credentialName);
output = output.replace(
new RegExp(credential.toString().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
`[${newCredential} Omitted]`
@@ -187,7 +225,7 @@ export default class EvalCommand extends BushCommand {
embed.addField('📥 Input', await this.client.util.codeblock(inputJS, 1024, 'js'));
}
embed.addField('📤 Output', await this.client.util.codeblock(output, 1024, 'js'));
- if (showProto) embed.addField('⚙️ Proto', await this.client.util.codeblock(outputProto, 1024, 'js'));
+ if (args.show_proto) embed.addField('⚙️ Proto', await this.client.util.codeblock(outputProto, 1024, 'js'));
} catch (e) {
const inputJS = clean(code.js);
embed
@@ -205,19 +243,21 @@ export default class EvalCommand extends BushCommand {
}
embed.addField('📤 Output', await this.client.util.codeblock(e?.stack, 1024, 'js'));
}
- if (!silent) {
- await message.util.reply({ embeds: [embed] });
+ if (!args.silent && !message.util.isSlash) {
+ await message.util.reply({ embeds: [embed], ephemeral: args.silent });
+ } else if (message.util.isSlash) {
+ await (message.interaction as CommandInteraction).editReply({ embeds: [embed] });
} else {
try {
await message.author.send({ embeds: [embed] });
- if (!deleteMSG) await message.react(this.client.util.emojis.successFull);
+ if (!args.deleteMSG) await (message as BushMessage).react(this.client.util.emojis.successFull);
} catch (e) {
- if (!deleteMSG) await message.react(this.client.util.emojis.errorFull);
+ if (!args.deleteMSG) await (message as BushMessage).react(this.client.util.emojis.errorFull);
}
}
- if (deleteMSG && message.deletable) {
- await message.delete();
+ if (args.deleteMSG && (message as BushMessage).deletable) {
+ await (message as BushMessage).delete();
}
}
}
diff --git a/src/commands/dev/reload.ts b/src/commands/dev/reload.ts
index 3194ce2..82a98a0 100644
--- a/src/commands/dev/reload.ts
+++ b/src/commands/dev/reload.ts
@@ -1,9 +1,9 @@
import { stripIndent } from 'common-tags';
import { ApplicationCommandOptionType } from 'discord-api-types';
import { Message } from 'discord.js';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
export default class ReloadCommand extends BushCommand {
constructor() {
@@ -28,7 +28,7 @@ export default class ReloadCommand extends BushCommand {
{
type: ApplicationCommandOptionType.BOOLEAN,
name: 'fast',
- description: 'Wheather to use esbuild for fast compiling or not',
+ description: 'Whether to use esbuild for fast compiling or not',
required: false
}
]
diff --git a/src/commands/dev/setLevel.ts b/src/commands/dev/setLevel.ts
index 7401699..4f97528 100644
--- a/src/commands/dev/setLevel.ts
+++ b/src/commands/dev/setLevel.ts
@@ -1,8 +1,8 @@
import { ApplicationCommandOptionType } from 'discord-api-types';
import { Message, User } from 'discord.js';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
import { Level } from '../../lib/models';
import AllowedMentions from '../../lib/utils/AllowedMentions';
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index 317091e..8dac8ee 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -1,9 +1,9 @@
import { stripIndent } from 'common-tags';
import { ApplicationCommandOptionType } from 'discord-api-types';
import { Message, MessageEmbed } from 'discord.js';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
export default class HelpCommand extends BushCommand {
constructor() {
diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts
index feb48ad..f0d017e 100644
--- a/src/commands/info/ping.ts
+++ b/src/commands/info/ping.ts
@@ -16,7 +16,7 @@ export default class PingCommand extends BushCommand {
}
public async exec(message: Message): Promise<void> {
- const sentMessage = await message.util.send('Pong!');
+ const sentMessage = await message.util.send('Pong!') as Message;
const timestamp: number = message.editedTimestamp ? message.editedTimestamp : message.createdTimestamp;
const botLatency = `\`\`\`\n ${Math.floor(sentMessage.createdTimestamp - timestamp)}ms \`\`\``;
const apiLatency = `\`\`\`\n ${Math.round(message.client.ws.ping)}ms \`\`\``;
diff --git a/src/commands/info/pronouns.ts b/src/commands/info/pronouns.ts
index faf3aa2..bade100 100644
--- a/src/commands/info/pronouns.ts
+++ b/src/commands/info/pronouns.ts
@@ -1,9 +1,9 @@
import { ApplicationCommandOptionType } from 'discord-api-types';
import { CommandInteraction, Message, MessageEmbed, User } from 'discord.js';
import got, { HTTPError } from 'got';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
export const pronounMapping = {
unspecified: 'Unspecified',
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
index f843ac4..4847d19 100644
--- a/src/commands/moderation/ban.ts
+++ b/src/commands/moderation/ban.ts
@@ -1,9 +1,9 @@
import { ApplicationCommandOptionType } from 'discord-api-types';
import { CommandInteraction, Message, User } from 'discord.js';
import moment from 'moment';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
import { Ban, Guild, Modlog, ModlogType } from '../../lib/models';
const durationAliases: Record<string, string[]> = {
@@ -133,8 +133,8 @@ export default class BanCommand extends BushCommand {
await modlogEnry.save();
await banEntry.save();
} catch (e) {
- console.error(e);
- yield 'Error saving to database. Please report this to a developer.';
+ this.client.console.error(`BanCommand`, `Error saving to database. ${e?.stack}`);
+ yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`;
return;
}
try {
@@ -144,18 +144,18 @@ export default class BanCommand extends BushCommand {
} with reason \`${reason || 'No reason given'}\``
);
} catch (e) {
- yield 'Error sending message to user';
+ yield `${this.client.util.emojis.warn} Unable to dm user`;
}
await message.guild.members.ban(user, {
reason: `Banned by ${message instanceof CommandInteraction ? message.user.tag : message.author.tag} with ${
reason ? `reason ${reason}` : 'no reason'
}`
});
- yield `Banned <@!${user.id}> ${
+ yield `${this.client.util.emojis.success} Banned <@!${user.id}> ${
translatedTime.length >= 1 ? `for ${translatedTime.join(', ')}` : 'permanently'
} with reason \`${reason || 'No reason given'}\``;
} catch {
- yield 'Error banning :/';
+ yield `${this.client.util.emojis.error} Error banning :/`;
await banEntry.destroy();
await modlogEnry.destroy();
return;
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index eed0122..7bd53e0 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -71,14 +71,14 @@ export default class KickCommand extends BushCommand {
});
await modlogEnry.save();
} catch (e) {
- console.error(e);
- yield 'Error saving to database. Please report this to a developer.';
+ this.client.console.error(`BanCommand`, `Error saving to database. ${e?.stack}`);
+ yield `${this.client.util.emojis.error} Error saving to database. Please report this to a developer.`;
return;
}
try {
await user.send(`You were kicked in ${message.guild.name} with reason \`${reason || 'No reason given'}\``);
} catch (e) {
- yield 'Error sending message to user';
+ yield `${this.client.util.emojis.warn} Unable to dm user`;
}
try {
await user.kick(
@@ -87,11 +87,11 @@ export default class KickCommand extends BushCommand {
}`
);
} catch {
- yield 'Error kicking :/';
+ yield `${this.client.util.emojis.error} Error kicking :/`;
await modlogEnry.destroy();
return;
}
- yield `Kicked <@!${user.id}> with reason \`${reason || 'No reason given'}\``;
+ yield `${this.client.util.emojis.success} Kicked <@!${user.id}> with reason \`${reason || 'No reason given'}\``;
}
async exec(message: Message, { user, reason }: { user: GuildMember; reason?: string }): Promise<void> {
diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts
index 8951560..1b82245 100644
--- a/src/commands/moderation/role.ts
+++ b/src/commands/moderation/role.ts
@@ -43,7 +43,7 @@ export default class RoleCommand extends BushCommand {
type: 'member',
prompt: {
start: `What user do you want to add/remove the role on?`,
- retry: `<:error:837123021016924261> Choose a valid user to add/remove the role on.`
+ retry: `{error} Choose a valid user to add/remove the role on.`
}
},
{
@@ -52,7 +52,7 @@ export default class RoleCommand extends BushCommand {
match: 'restContent',
prompt: {
start: `What role do you want to add/remove?`,
- retry: `<:error:837123021016924261> Choose a valid role to add/remove.`
+ retry: `{error} Choose a valid role to add/remove.`
}
}
],
@@ -78,7 +78,7 @@ export default class RoleCommand extends BushCommand {
const mappedRole = this.client.util.moulberryBushRoleMap.find((m) => m.id === role.id);
if (!mappedRole || !this.roleWhitelist[mappedRole.name]) {
return message.util.reply({
- content: `<:error:837123021016924261> <@&${role.id}> is not whitelisted, and you do not have manage roles permission.`,
+ content: `${this.client.util.emojis.error} <@&${role.id}> is not whitelisted, and you do not have manage roles permission.`,
allowedMentions: AllowedMentions.none()
});
}
@@ -87,7 +87,7 @@ export default class RoleCommand extends BushCommand {
});
if (!message.member.roles.cache.some((role) => allowedRoles.includes(role.id))) {
return message.util.reply({
- content: `<:error:837123021016924261> <@&${role.id}> is whitelisted, but you do not have any of the roles required to manage it.`,
+ content: `${this.client.util.emojis.error} <@&${role.id}> is whitelisted, but you do not have any of the roles required to manage it.`,
allowedMentions: AllowedMentions.none()
});
}
@@ -95,19 +95,19 @@ export default class RoleCommand extends BushCommand {
if (!this.client.ownerID.includes(message.author.id)) {
if (role.comparePositionTo(message.member.roles.highest) >= 0) {
return message.util.reply({
- content: `<:error:837123021016924261> <@&${role.id}> is higher or equal to your highest role.`,
+ content: `${this.client.util.emojis.error} <@&${role.id}> is higher or equal to your highest role.`,
allowedMentions: AllowedMentions.none()
});
}
if (role.comparePositionTo(message.guild.me.roles.highest) >= 0) {
return message.util.reply({
- content: `<:error:837123021016924261> <@&${role.id}> is higher or equal to my highest role.`,
+ content: `${this.client.util.emojis.error} <@&${role.id}> is higher or equal to my highest role.`,
allowedMentions: AllowedMentions.none()
});
}
if (role.managed) {
await message.util.reply({
- content: `<:error:837123021016924261> <@&${role.id}> is managed by an integration and cannot be managed.`,
+ content: `${this.client.util.emojis.error} <@&${role.id}> is managed by an integration and cannot be managed.`,
allowedMentions: AllowedMentions.none()
});
}
@@ -118,12 +118,12 @@ export default class RoleCommand extends BushCommand {
await user.roles.remove(role.id);
} catch {
return message.util.reply({
- content: `<:error:837123021016924261> Could not remove <@&${role.id}> from <@${user.id}>.`,
+ content: `${this.client.util.emojis.error} Could not remove <@&${role.id}> from <@${user.id}>.`,
allowedMentions: AllowedMentions.none()
});
}
return message.util.reply({
- content: `<:checkmark:837109864101707807> Successfully removed <@&${role.id}> from <@${user.id}>!`,
+ content: `${this.client.util.emojis.success} Successfully removed <@&${role.id}> from <@${user.id}>!`,
allowedMentions: AllowedMentions.none()
});
} else {
@@ -131,12 +131,12 @@ export default class RoleCommand extends BushCommand {
await user.roles.add(role.id);
} catch {
return message.util.reply({
- content: `<:error:837123021016924261> Could not add <@&${role.id}> to <@${user.id}>.`,
+ content: `${this.client.util.emojis.error} Could not add <@&${role.id}> to <@${user.id}>.`,
allowedMentions: AllowedMentions.none()
});
}
return message.util.reply({
- content: `<:checkmark:837109864101707807> Successfully added <@&${role.id}> to <@${user.id}>!`,
+ content: `${this.client.util.emojis.success} Successfully added <@&${role.id}> to <@${user.id}>!`,
allowedMentions: AllowedMentions.none()
});
}
diff --git a/src/commands/moulberry-bush/capePerms.ts b/src/commands/moulberry-bush/capePerms.ts
index 7eb90c5..380ed2d 100644
--- a/src/commands/moulberry-bush/capePerms.ts
+++ b/src/commands/moulberry-bush/capePerms.ts
@@ -1,9 +1,9 @@
import { ApplicationCommandOptionType } from 'discord-api-types';
import { Message, MessageEmbed } from 'discord.js';
import got from 'got';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
interface Capeperms {
success: boolean;
@@ -57,7 +57,7 @@ export default class CapePermissionsCommand extends BushCommand {
type: 'string',
prompt: {
start: 'Who would you like to see the cape permissions of?',
- retry: '<:error:837123021016924261> Choose someone to see the capes their available capes.',
+ retry: '{error} Choose someone to see the capes their available capes.',
optional: false
}
}
@@ -79,7 +79,7 @@ export default class CapePermissionsCommand extends BushCommand {
try {
uuid = await this.client.util.mcUUID(user);
} catch (e) {
- return { content: `<:error:837123021016924261> \`${user}\` doesn't appear to be a valid username.` };
+ return { content: `${this.client.util.emojis.error} \`${user}\` doesn't appear to be a valid username.` };
}
try {
@@ -88,11 +88,12 @@ export default class CapePermissionsCommand extends BushCommand {
capeperms = null;
}
if (capeperms == null) {
- return { content: `<:error:837123021016924261> There was an error finding cape perms for \`${user}\`.` };
+ return { content: `${this.client.util.emojis.error} There was an error finding cape perms for \`${user}\`.` };
} else {
if (capeperms?.perms) {
const foundUser = capeperms.perms.find((u) => u._id === uuid);
- if (foundUser == null) return { content: `<:error:837123021016924261> \`${user}\` does not appear to have any capes.` };
+ if (foundUser == null)
+ return { content: `${this.client.util.emojis.error} \`${user}\` does not appear to have any capes.` };
const userPerm: string[] = foundUser.perms;
const embed = this.client.util
.createEmbed(this.client.util.colors.default)
@@ -100,7 +101,7 @@ export default class CapePermissionsCommand extends BushCommand {
.setDescription(userPerm.join('\n'));
return { embeds: [embed] };
} else {
- return { content: `<:error:837123021016924261> There was an error finding cape perms for ${user}.` };
+ return { content: `${this.client.util.emojis.error} There was an error finding cape perms for ${user}.` };
}
}
}
diff --git a/src/commands/moulberry-bush/giveawayPing.ts b/src/commands/moulberry-bush/giveawayPing.ts
index 9a03140..d308602 100644
--- a/src/commands/moulberry-bush/giveawayPing.ts
+++ b/src/commands/moulberry-bush/giveawayPing.ts
@@ -24,9 +24,9 @@ export default class GiveawayPingCommand extends BushCommand {
}
public async exec(message: Message): Promise<unknown> {
if (message.guild.id !== '516977525906341928')
- return message.reply("<:error:837123021016924261> This command may only be run in Moulberry's Bush.");
+ return message.reply(`${this.client.util.emojis.error} This command may only be run in Moulberry's Bush.`);
if (!['767782084981817344', '833855738501267456'].includes(message.channel.id))
- return message.reply('<:error:837123021016924261> This command may only be run in giveaway channels.');
+ return message.reply(`${this.client.util.emojis.error} This command may only be run in giveaway channels.`);
await message.delete().catch(() => undefined);
const webhooks = await (message.channel as TextChannel | NewsChannel).fetchWebhooks();
let webhookClient: WebhookClient;
@@ -38,8 +38,7 @@ export default class GiveawayPingCommand extends BushCommand {
webhookClient = new WebhookClient(webhook.id, webhook.token);
}
return webhookClient.send({
- content:
- '🎉 <@&767782793261875210> Giveaway.\n\n<:mad:783046135392239626> Spamming, line breaking, gibberish etc. disqualifies you from winning. We can and will ban you from giveaways. Winners will all be checked and rerolled if needed.',
+ content: `🎉 <@&767782793261875210> Giveaway.\n\n${this.client.util.emojis.mad} Spamming, line breaking, gibberish etc. disqualifies you from winning. We can and will ban you from giveaways. Winners will all be checked and rerolled if needed.`,
username: `${message.member.nickname || message.author.username}`,
avatarURL: message.author.avatarURL({ dynamic: true }),
allowedMentions: AllowedMentions.roles()
diff --git a/src/commands/moulberry-bush/rule.ts b/src/commands/moulberry-bush/rule.ts
index b71b42f..674b776 100644
--- a/src/commands/moulberry-bush/rule.ts
+++ b/src/commands/moulberry-bush/rule.ts
@@ -1,9 +1,9 @@
import { Argument } from 'discord-akairo';
import { ApplicationCommandOptionType } from 'discord-api-types';
import { CommandInteraction, Message, MessageEmbed, User } from 'discord.js';
+import { SlashCommandOption } from '../../lib/extensions/BushClientUtil';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushInteractionMessage } from '../../lib/extensions/BushInteractionMessage';
-import { SlashCommandOption } from '../../lib/extensions/Util';
export default class RuleCommand extends BushCommand {
private rules = [
@@ -80,7 +80,7 @@ export default class RuleCommand extends BushCommand {
type: Argument.range('number', 1, 12, true),
prompt: {
start: 'What rule would you like to have cited?',
- retry: '<:no:787549684196704257> Choose a valid rule.',
+ retry: '{error} Choose a valid rule.',
optional: true
},
default: undefined
@@ -90,7 +90,7 @@ export default class RuleCommand extends BushCommand {
type: 'user',
prompt: {
start: 'What user would you like to mention?',
- retry: '<:no:787549684196704257> Choose a valid user to mention.',
+ retry: '{error} Choose a valid user to mention.',
optional: true
},
default: undefined
@@ -123,7 +123,7 @@ export default class RuleCommand extends BushCommand {
message.guild.id !== '516977525906341928' &&
!this.client.ownerID.includes(message instanceof Message ? message.author.id : message.user.id)
) {
- return { content: "<:no:787549684196704257> This command can only be run in Moulberry's Bush." };
+ return { content: `${this.client.util.emojis.error} This command can only be run in Moulberry's Bush.` };
}
let rulesEmbed = new MessageEmbed().setColor('ef3929');
if (message instanceof Message) {
diff --git a/src/config/example-options.ts b/src/config/example-options.ts
index a00ba42..ce8f782 100644
--- a/src/config/example-options.ts
+++ b/src/config/example-options.ts
@@ -2,7 +2,9 @@ import { Snowflake } from 'discord.js';
// Credentials
export const credentials = {
- botToken: 'token here'
+ token: 'Token Here',
+ devToken: 'Token Here',
+ hypixelApiKey: 'API Key Here'
};
// Options
diff --git a/src/lib/extensions/BushClient.ts b/src/lib/extensions/BushClient.ts
index e2e889b..1bd3493 100644
--- a/src/lib/extensions/BushClient.ts
+++ b/src/lib/extensions/BushClient.ts
@@ -1,26 +1,30 @@
import chalk from 'chalk';
-import { AkairoClient, InhibitorHandler, ListenerHandler, TaskHandler } from 'discord-akairo';
-import { Guild, Intents, Snowflake } from 'discord.js';
+import { AkairoClient, TaskHandler } from 'discord-akairo';
+import { APIMessage, Guild, Intents, Message, MessageOptions, Snowflake, UserResolvable } from 'discord.js';
import * as path from 'path';
import { exit } from 'process';
import { Sequelize } from 'sequelize';
import * as config from '../../config/options';
import * as Models from '../models';
import AllowedMentions from '../utils/AllowedMentions';
-import { BushLogger } from '../utils/Logger';
+import { BushCache } from '../utils/BushCache';
+import { BushLogger } from '../utils/BushLogger';
+import { BushClientUtil } from './BushClientUtil';
import { BushCommandHandler } from './BushCommandHandler';
-import { BushUtil } from './Util';
+import { BushInhibitorHandler } from './BushInhinitorHandler';
+import { BushListenerHandler } from './BushListenerHandler';
export type BotConfig = typeof config;
+export type BushMessageType = string | APIMessage | (MessageOptions & { split?: false });
export class BushClient extends AkairoClient {
public config: BotConfig;
- public listenerHandler: ListenerHandler;
- public inhibitorHandler: InhibitorHandler;
+ public listenerHandler: BushListenerHandler;
+ public inhibitorHandler: BushInhibitorHandler;
public commandHandler: BushCommandHandler;
public taskHandler: TaskHandler;
- public util: BushUtil;
- public ownerID: Snowflake[];
+ public declare util: BushClientUtil;
+ public declare ownerID: Snowflake[];
public db: Sequelize;
public logger: BushLogger;
constructor(config: BotConfig) {
@@ -36,19 +40,19 @@ export class BushClient extends AkairoClient {
);
// Set token
- this.token = config.credentials.botToken;
+ this.token = config.credentials.token;
// Set config
this.config = config;
// Create listener handler
- this.listenerHandler = new ListenerHandler(this, {
+ this.listenerHandler = new BushListenerHandler(this, {
directory: path.join(__dirname, '..', '..', 'listeners'),
automateCategories: true
});
// Create inhibitor handler
- this.inhibitorHandler = new InhibitorHandler(this, {
+ this.inhibitorHandler = new BushInhibitorHandler(this, {
directory: path.join(__dirname, '..', '..', 'inhibitors'),
automateCategories: true
});
@@ -72,18 +76,24 @@ export class BushClient extends AkairoClient {
commandUtilLifetime: 3e5,
argumentDefaults: {
prompt: {
- timeout: 'Timed out.',
- ended: 'Too many tries.',
- cancel: 'Canceled.',
+ start: 'Placeholder argument prompt. If you see this please tell the devs.',
+ retry: 'Placeholder failed argument prompt. If you see this please tell the devs.',
+ modifyStart: (_: Message, str: string): string => `${str}\n\n Type \`cancel\` to cancel the command`,
+ modifyRetry: (_: Message, str: string): string =>
+ `${str.replace('{error}', this.util.emojis.error)}\n\n Type \`cancel\` to cancel the command`,
+ timeout: 'You took too long the command has been cancelled',
+ ended: 'You exceeded the maximum amount of tries the command has been cancelled',
+ cancel: 'The command has been cancelled',
+ retries: 3,
time: 3e4
- }
+ },
+ otherwise: ''
},
ignorePermissions: this.config.owners,
- ignoreCooldown: this.config.owners,
- automateCategories: true
+ ignoreCooldown: this.config.owners
});
- this.util = new BushUtil(this);
+ this.util = new BushClientUtil(this);
this.db = new Sequelize(this.config.dev ? 'bushbot-dev' : 'bushbot', this.config.db.username, this.config.db.password, {
dialect: 'postgres',
host: this.config.db.host,
@@ -93,6 +103,10 @@ export class BushClient extends AkairoClient {
this.logger = new BushLogger(this);
}
+ get console(): BushLogger {
+ return this.logger;
+ }
+
// Initialize everything
private async _init(): Promise<void> {
this.commandHandler.useListenerHandler(this.listenerHandler);
@@ -112,9 +126,9 @@ export class BushClient extends AkairoClient {
for (const loader of Object.keys(loaders)) {
try {
loaders[loader].loadAll();
- this.logger.log(chalk.green('Successfully loaded ' + chalk.cyan(loader) + '.'));
+ this.logger.success('Startup', `Successfully loaded <<${loader}>>.` + chalk.cyan() + '.', false);
} catch (e) {
- console.error(chalk.red('Unable to load loader ' + chalk.cyan(loader) + ' with error ' + e));
+ this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false);
}
}
this.taskHandler.startAll();
@@ -122,12 +136,14 @@ export class BushClient extends AkairoClient {
}
public async dbPreInit(): Promise<void> {
- await this.db.authenticate();
- Models.Guild.initModel(this.db, this);
- Models.Modlog.initModel(this.db);
- Models.Ban.initModel(this.db);
- Models.Level.initModel(this.db);
- await this.db.sync(); // Sync all tables to fix everything if updated
+ try {
+ await this.db.authenticate();
+ Models.Guild.initModel(this.db, this);
+ Models.Modlog.initModel(this.db);
+ Models.Ban.initModel(this.db);
+ Models.Level.initModel(this.db);
+ await this.db.sync(); // Sync all tables to fix everything if updated
+ } catch (error) {}
}
public async start(): Promise<void> {
@@ -135,7 +151,7 @@ export class BushClient extends AkairoClient {
await this._init();
await this.login(this.token);
} catch (e) {
- console.error(chalk.red(e.stack));
+ this.console.error('Start', chalk.red(e.stack), false);
exit(2);
}
}
@@ -146,4 +162,12 @@ export class BushClient extends AkairoClient {
return this.login(this.token);
}
}
+
+ public isOwner(user: UserResolvable): boolean {
+ return this.config.owners.includes(this.users.resolveID(user));
+ }
+ public isSuperUser(user: UserResolvable): boolean {
+ const userID = this.users.resolveID(user);
+ return !!BushCache?.superUsers?.includes(userID) || this.config.owners.includes(userID);
+ }
}
diff --git a/src/lib/extensions/BushClientUtil.ts b/src/lib/extensions/BushClientUtil.ts
new file mode 100644
index 0000000..9b87efd
--- /dev/null
+++ b/src/lib/extensions/BushClientUtil.ts
@@ -0,0 +1,879 @@
+import { exec } from 'child_process';
+import { ClientUtil, Command } from 'discord-akairo';
+import {
+ APIInteractionDataResolvedChannel,
+ APIInteractionDataResolvedGuildMember,
+ APIMessage,
+ APIRole,
+ ApplicationCommandOptionType
+} from 'discord-api-types';
+import {
+ ButtonInteraction,
+ CommandInteraction,
+ CommandInteractionOption,
+ Constants,
+ Guild,
+ GuildChannel,
+ GuildMember,
+ InteractionReplyOptions,
+ Message,
+ MessageActionRow,
+ MessageButton,
+ MessageComponentInteraction,
+ MessageEditOptions,
+ MessageEmbed,
+ MessageOptions,
+ Role,
+ Snowflake,
+ TextChannel,
+ User,
+ Util,
+ WebhookEditMessageOptions
+} from 'discord.js';
+import got from 'got';
+import { promisify } from 'util';
+import { BushClient } from './BushClient';
+import { BushMessage } from './BushMessage';
+
+interface hastebinRes {
+ key: string;
+}
+
+export interface uuidRes {
+ uuid: string;
+ username: string;
+ username_history?: { username: string }[] | null;
+ textures: {
+ custom: boolean;
+ slim: boolean;
+ skin: {
+ url: string;
+ data: string;
+ };
+ raw: {
+ value: string;
+ signature: string;
+ };
+ };
+ created_at: string;
+}
+
+export interface SlashCommandOption<T> {
+ name: string;
+ type: ApplicationCommandOptionType;
+ value?: T;
+ options?: CommandInteractionOption[];
+ user?: User;
+ member?: GuildMember | APIInteractionDataResolvedGuildMember;
+ channel?: GuildChannel | APIInteractionDataResolvedChannel;
+ role?: Role | APIRole;
+}
+
+export class BushClientUtil extends ClientUtil {
+ /** The client of this ClientUtil */
+ public declare client: BushClient;
+ /** The hastebin urls used to post to hastebin, attempts to post in order */
+ public hasteURLs: string[] = [
+ 'https://hst.sh',
+ 'https://hasteb.in',
+ 'https://hastebin.com',
+ 'https://mystb.in',
+ 'https://haste.clicksminuteper.net',
+ 'https://paste.pythondiscord.com',
+ 'https://haste.unbelievaboat.com',
+ 'https://haste.tyman.tech'
+ ];
+ public paginateEmojis = {
+ beginning: '853667381335162910',
+ back: '853667410203770881',
+ stop: '853667471110570034',
+ forward: '853667492680564747',
+ end: '853667514915225640'
+ };
+
+ /** A simple promise exec method */
+ private exec = promisify(exec);
+
+ /**
+ * Creates this client util
+ * @param client The client to initialize with
+ */
+ constructor(client: BushClient) {
+ super(client);
+ }
+
+ /**
+ * Maps an array of user ids to user objects.
+ * @param ids The list of IDs to map
+ * @returns The list of users mapped
+ */
+ public async mapIDs(ids: Snowflake[]): Promise<User[]> {
+ return await Promise.all(ids.map((id) => this.client.users.fetch(id)));
+ }
+
+ /**
+ * Capitalizes the first letter of the given text
+ * @param text The text to capitalize
+ * @returns The capitalized text
+ */
+ public capitalize(text: string): string {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+ }
+
+ /**
+ * Runs a shell command and gives the output
+ * @param command The shell command to run
+ * @returns The stdout and stderr of the shell command
+ */
+ public async shell(command: string): Promise<{
+ stdout: string;
+ stderr: string;
+ }> {
+ return await this.exec(command);
+ }
+
+ /**
+ * Posts text to hastebin
+ * @param content The text to post
+ * @returns The url of the posted text
+ */
+ public async haste(content: string): Promise<string> {
+ for (const url of this.hasteURLs) {
+ try {
+ const res: hastebinRes = await got.post(`${url}/documents`, { body: content }).json();
+ return `${url}/${res.key}`;
+ } catch (e) {
+ this.client.console.error('Haste', `Unable to upload haste to ${url}`);
+ continue;
+ }
+ }
+ return 'Unable to post';
+ }
+
+ /**
+ * Resolves a user-provided string into a user object, if possible
+ * @param text The text to try and resolve
+ * @returns The user resolved or null
+ */
+ public async resolveUserAsync(text: string): Promise<User | null> {
+ const idReg = /\d{17,19}/;
+ const idMatch = text.match(idReg);
+ if (idMatch) {
+ try {
+ const user = await this.client.users.fetch(text as Snowflake);
+ return user;
+ } catch {
+ // pass
+ }
+ }
+ const mentionReg = /<@!?(?<id>\d{17,19})>/;
+ const mentionMatch = text.match(mentionReg);
+ if (mentionMatch) {
+ try {
+ const user = await this.client.users.fetch(mentionMatch.groups.id as Snowflake);
+ return user;
+ } catch {
+ // pass
+ }
+ }
+ const user = this.client.users.cache.find((u) => u.username === text);
+ if (user) return user;
+ return null;
+ }
+
+ /**
+ * Appends the correct ordinal to the given number
+ * @param n The number to append an ordinal to
+ * @returns The number with the ordinal
+ */
+ public ordinal(n: number): string {
+ const s = ['th', 'st', 'nd', 'rd'],
+ v = n % 100;
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
+ }
+
+ /**
+ * Chunks an array to the specified size
+ * @param arr The array to chunk
+ * @param perChunk The amount of items per chunk
+ * @returns The chunked array
+ */
+ public chunk<T>(arr: T[], perChunk: number): T[][] {
+ return arr.reduce((all, one, i) => {
+ const ch = Math.floor(i / perChunk);
+ all[ch] = [].concat(all[ch] || [], one);
+ return all;
+ }, []);
+ }
+
+ /** Commonly Used Colors */
+ public colors = {
+ 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 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>'
+ };
+
+ /**
+ * A simple utility to create and embed with the needed style for the bot
+ */
+ public createEmbed(color?: string, author?: User | GuildMember): MessageEmbed {
+ if (author instanceof GuildMember) {
+ author = author.user; // Convert to User if GuildMember
+ }
+ let embed = new MessageEmbed().setTimestamp();
+ if (author)
+ embed = embed.setAuthor(
+ author.username,
+ author.displayAvatarURL({ dynamic: true }),
+ `https://discord.com/users/${author.id}`
+ );
+ if (color) embed = embed.setColor(color);
+ return embed;
+ }
+
+ public async mcUUID(username: string): Promise<string> {
+ const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as uuidRes;
+ return apiRes.uuid.replace(/-/g, '');
+ }
+
+ public async syncSlashCommands(force = false, guild?: Snowflake): Promise<void> {
+ let fetchedGuild: Guild;
+ if (guild) fetchedGuild = this.client.guilds.cache.get(guild);
+ try {
+ const registered =
+ guild === undefined ? await this.client.application.commands.fetch() : await fetchedGuild.commands.fetch();
+ for (const [, registeredCommand] of registered) {
+ if (!this.client.commandHandler.modules.find((cmd) => cmd.id == registeredCommand.name)?.execSlash || force) {
+ guild === undefined
+ ? await this.client.application.commands.delete(registeredCommand.id)
+ : await fetchedGuild.commands.delete(registeredCommand.id);
+ this.client.logger.verbose(
+ 'syncSlashCommands',
+ `Deleted slash command <<${registeredCommand.name}>>${
+ guild !== undefined ? ` in guild <<${fetchedGuild.name}>>` : ''
+ }`
+ );
+ }
+ }
+
+ for (const [, botCommand] of this.client.commandHandler.modules) {
+ if (botCommand.execSlash) {
+ const found = registered.find((i) => i.name == botCommand.id);
+ Command;
+ const slashdata = {
+ name: botCommand.id,
+ description: botCommand.description.content,
+ options: botCommand.options.slashCommandOptions
+ };
+ botCommand;
+
+ if (found?.id && !force) {
+ if (slashdata.description !== found.description) {
+ guild === undefined
+ ? await this.client.application.commands.edit(found.id, slashdata)
+ : fetchedGuild.commands.edit(found.id, slashdata);
+ this.client.logger.verbose(
+ 'syncSlashCommands',
+ `Edited slash command <<${botCommand.id}>>${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}`
+ );
+ }
+ } else {
+ guild === undefined
+ ? await this.client.application.commands.create(slashdata)
+ : fetchedGuild.commands.create(slashdata);
+ this.client.logger.verbose(
+ 'syncSlashCommands',
+ `Created slash command <<${botCommand.id}>>${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}}`
+ );
+ }
+ }
+ }
+
+ return this.client.logger.log(
+ 'syncSlashCommands',
+ `Slash commands registered${guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''}`
+ );
+ } catch (e) {
+ return this.client.logger.error(
+ 'syncSlashCommands',
+ `Slash commands not registered${
+ guild !== undefined ? ` in guild <<${fetchedGuild?.name}>>` : ''
+ }, with the following error:\n${e?.stack}`
+ );
+ }
+ }
+
+ public moulberryBushRoleMap = [
+ { name: '*', id: '792453550768390194' },
+ { name: 'Admin Perms', id: '746541309853958186' },
+ { name: 'Sr. Moderator', id: '782803470205190164' },
+ { name: 'Moderator', id: '737308259823910992' },
+ { name: 'Helper', id: '737440116230062091' },
+ { name: 'Trial Helper', id: '783537091946479636' },
+ { name: 'Contributor', id: '694431057532944425' },
+ { name: 'Giveaway Donor', id: '784212110263451649' },
+ { name: 'Giveaway (200m)', id: '810267756426690601' },
+ { name: 'Giveaway (100m)', id: '801444430522613802' },
+ { name: 'Giveaway (50m)', id: '787497512981757982' },
+ { name: 'Giveaway (25m)', id: '787497515771232267' },
+ { name: 'Giveaway (10m)', id: '787497518241153025' },
+ { name: 'Giveaway (5m)', id: '787497519768403989' },
+ { name: 'Giveaway (1m)', id: '787497521084891166' },
+ { name: 'Suggester', id: '811922322767609877' },
+ { name: 'Partner', id: '767324547312779274' },
+ { name: 'Level Locked', id: '784248899044769792' },
+ { name: 'No Files', id: '786421005039173633' },
+ { name: 'No Reactions', id: '786421270924361789' },
+ { name: 'No Links', id: '786421269356740658' },
+ { name: 'No Bots', id: '786804858765312030' },
+ { name: 'No VC', id: '788850482554208267' },
+ { name: 'No Giveaways', id: '808265422334984203' },
+ { name: 'No Support', id: '790247359824396319' }
+ ];
+
+ /** Paginates an array of embeds using buttons. */
+ public async buttonPaginate(
+ message: BushMessage,
+ embeds: MessageEmbed[],
+ text: string | null = null,
+ deleteOnExit?: boolean
+ ): Promise<void> {
+ if (deleteOnExit === undefined) deleteOnExit = true;
+
+ embeds.forEach((_e, i) => {
+ embeds[i] = embeds[i].setFooter(`Page ${i + 1}/${embeds.length}`);
+ });
+
+ const style = Constants.MessageButtonStyles.PRIMARY;
+ let curPage = 0;
+ if (typeof embeds !== 'object') throw 'embeds must be an object';
+ const msg = (await message.util.reply({
+ content: text,
+ embeds: [embeds[curPage]],
+ components: [getPaginationRow()]
+ })) as Message;
+ const filter = (interaction: ButtonInteraction) =>
+ interaction.customID.startsWith('paginate_') && interaction.message == msg;
+ const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 });
+ collector.on('collect', async (interaction: MessageComponentInteraction) => {
+ if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
+ switch (interaction.customID) {
+ case 'paginate_beginning': {
+ curPage = 0;
+ await edit(interaction);
+ break;
+ }
+ case 'paginate_back': {
+ curPage--;
+ await edit(interaction);
+ break;
+ }
+ case 'paginate_stop': {
+ if (deleteOnExit) {
+ await interaction.deferUpdate().catch(() => {});
+ if (msg.deletable && !msg.deleted) {
+ await msg.delete();
+ }
+ } else {
+ await interaction
+ ?.update({ content: `${text ? text + '\n' : ''}Command closed by user.`, embeds: [], components: [] })
+ .catch(() => {});
+ }
+ return;
+ }
+ case 'paginate_next': {
+ curPage++;
+ await edit(interaction);
+ break;
+ }
+ case 'paginate_end': {
+ curPage = embeds.length - 1;
+ await edit(interaction);
+ break;
+ }
+ }
+ } else {
+ return await interaction?.deferUpdate().catch(() => {});
+ }
+ });
+
+ collector.on('end', async () => {
+ await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => {});
+ });
+
+ async function edit(interaction: MessageComponentInteraction): Promise<void> {
+ return await interaction
+ ?.update({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] })
+ .catch(() => {});
+ }
+ const paginateEmojis = this.paginateEmojis;
+ function getPaginationRow(disableAll = false): MessageActionRow {
+ return new MessageActionRow().addComponents(
+ new MessageButton({
+ style,
+ customID: 'paginate_beginning',
+ emoji: paginateEmojis.beginning,
+ disabled: disableAll || curPage == 0
+ }),
+ new MessageButton({
+ style,
+ customID: 'paginate_back',
+ emoji: paginateEmojis.back,
+ disabled: disableAll || curPage == 0
+ }),
+ new MessageButton({ style, customID: 'paginate_stop', emoji: paginateEmojis.stop, disabled: disableAll }),
+ new MessageButton({
+ style,
+ customID: 'paginate_next',
+ emoji: paginateEmojis.forward,
+ disabled: disableAll || curPage == embeds.length - 1
+ }),
+ new MessageButton({
+ style,
+ customID: 'paginate_end',
+ emoji: paginateEmojis.end,
+ disabled: disableAll || curPage == embeds.length - 1
+ })
+ );
+ }
+ }
+
+ /** Sends a message with a button for the user to delete it. */
+ public async sendWithDeleteButton(message: BushMessage, options: MessageOptions): Promise<void> {
+ updateOptions();
+ const msg = (await message.util.reply(options as MessageOptions & { split?: false })) as Message;
+ const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg;
+ const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 });
+ collector.on('collect', async (interaction: MessageComponentInteraction) => {
+ if (interaction.user.id == message.author.id || this.client.config.owners.includes(interaction.user.id)) {
+ await interaction.deferUpdate().catch(() => {});
+ if (msg.deletable && !msg.deleted) {
+ await msg.delete();
+ }
+ return;
+ } else {
+ return await interaction?.deferUpdate().catch(() => {});
+ }
+ });
+
+ collector.on('end', async () => {
+ updateOptions(true, true);
+ await msg.edit(options as MessageEditOptions).catch(() => {});
+ });
+
+ const paginateEmojis = this.paginateEmojis;
+ function updateOptions(edit?: boolean, disable?: boolean) {
+ if (edit == undefined) edit = false;
+ if (disable == undefined) disable = false;
+ options.components = [
+ new MessageActionRow().addComponents(
+ new MessageButton({
+ style: Constants.MessageButtonStyles.PRIMARY,
+ customID: 'paginate__stop',
+ emoji: paginateEmojis.stop,
+ disabled: disable
+ })
+ )
+ ];
+ if (edit) {
+ options.reply = undefined;
+ }
+ }
+ }
+
+ /**
+ * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long.
+ *
+ * * Embed Description Limit = 2048 characters
+ * * Embed Field Limit = 1024 characters
+ */
+ public async codeblock(code: string, length: number, language: 'ts' | 'js' | 'sh' | 'json' | '' = ''): Promise<string> {
+ let hasteOut = '';
+ const tildes = '```';
+ const formattingLength = 2 * tildes.length + language.length + 2 * '\n'.length;
+ if (code.length + formattingLength > length) hasteOut = 'Too large to display. Hastebin: ' + (await this.haste(code));
+
+ const code2 = code.length > length ? code.substring(0, length - (hasteOut.length + '\n'.length + formattingLength)) : code;
+ return (
+ tildes + language + '\n' + Util.cleanCodeBlockContent(code2) + '\n' + tildes + (hasteOut.length ? '\n' + hasteOut : '')
+ );
+ }
+
+ public async slashRespond(
+ interaction: CommandInteraction,
+ responseOptions: string | InteractionReplyOptions
+ ): Promise<Message | APIMessage | void> {
+ let newResponseOptions: InteractionReplyOptions | WebhookEditMessageOptions = {};
+ if (typeof responseOptions === 'string') {
+ newResponseOptions.content = responseOptions;
+ } else {
+ newResponseOptions = responseOptions;
+ }
+ if (interaction.replied || interaction.deferred) {
+ //@ts-expect-error: stop being dumb
+ delete newResponseOptions.ephemeral; // Cannot change a preexisting message to be ephemeral
+ return (await interaction.editReply(newResponseOptions)) as APIMessage;
+ } else {
+ await interaction.reply(newResponseOptions);
+ return await interaction.fetchReply().catch(() => {});
+ }
+ }
+
+ /** Gets the channel configs as a TextChannel */
+ public getConfigChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> {
+ return this.client.channels.fetch(this.client.config.channels[channel]) as Promise<TextChannel>;
+ }
+
+ /** A bunch of mappings */
+ public mappings = {
+ guilds: {
+ bush: '516977525906341928',
+ tree: '767448775450820639',
+ staff: '784597260465995796',
+ space_ship: '717176538717749358',
+ sbr: '839287012409999391'
+ },
+
+ permissions: {
+ CREATE_INSTANT_INVITE: { name: 'Create Invite', important: false },
+ KICK_MEMBERS: { name: 'Kick Members', important: true },
+ BAN_MEMBERS: { name: 'Ban Members', important: true },
+ ADMINISTRATOR: { name: 'Administrator', important: true },
+ MANAGE_CHANNELS: { name: 'Manage Channels', important: true },
+ MANAGE_GUILD: { name: 'Manage Server', important: true },
+ ADD_REACTIONS: { name: 'Add Reactions', important: false },
+ VIEW_AUDIT_LOG: { name: 'View Audit Log', important: true },
+ PRIORITY_SPEAKER: { name: 'Priority Speaker', important: true },
+ STREAM: { name: 'Video', important: false },
+ VIEW_CHANNEL: { name: 'View Channel', important: false },
+ SEND_MESSAGES: { name: 'Send Messages', important: false },
+ SEND_TTS_MESSAGES: { name: 'Send Text-to-Speech Messages', important: true },
+ MANAGE_MESSAGES: { name: 'Manage Messages', important: true },
+ EMBED_LINKS: { name: 'Embed Links', important: false },
+ ATTACH_FILES: { name: 'Attach Files', important: false },
+ READ_MESSAGE_HISTORY: { name: 'Read Message History', important: false },
+ MENTION_EVERYONE: { name: 'Mention @​everyone, @​here, and All Roles', important: true }, // name has a zero-width space to prevent accidents
+ USE_EXTERNAL_EMOJIS: { name: 'Use External Emoji', important: false },
+ VIEW_GUILD_INSIGHTS: { name: 'View Server Insights', important: true },
+ CONNECT: { name: 'Connect', important: false },
+ SPEAK: { name: 'Speak', important: false },
+ MUTE_MEMBERS: { name: 'Mute Members', important: true },
+ DEAFEN_MEMBERS: { name: 'Deafen Members', important: true },
+ MOVE_MEMBERS: { name: 'Move Members', important: true },
+ USE_VAD: { name: 'Use Voice Activity', important: false },
+ CHANGE_NICKNAME: { name: 'Change Nickname', important: false },
+ MANAGE_NICKNAMES: { name: 'Change Nicknames', important: true },
+ MANAGE_ROLES: { name: 'Manage Roles', important: true },
+ MANAGE_WEBHOOKS: { name: 'Manage Webhooks', important: true },
+ MANAGE_EMOJIS: { name: 'Manage Emojis', important: true },
+ USE_APPLICATION_COMMANDS: { name: 'Use Slash Commands', important: false },
+ REQUEST_TO_SPEAK: { name: 'Request to Speak', important: false },
+ USE_PUBLIC_THREADS: { name: 'Use Public Threads', important: false },
+ USE_PRIVATE_THREADS: { name: 'Use Private Threads', important: true }
+ },
+
+ features: {
+ ANIMATED_ICON: { name: 'Animated Icon', important: false, emoji: '<:animatedIcon:850774498071412746>', weight: 14 },
+ BANNER: { name: 'Banner', important: false, emoji: '<:banner:850786673150787614>', weight: 15 },
+ COMMERCE: { name: 'Store Channels', important: true, emoji: '<:storeChannels:850786692432396338>', weight: 11 },
+ COMMUNITY: { name: 'Community', important: false, emoji: '<:community:850786714271875094>', weight: 20 },
+ DISCOVERABLE: { name: 'Discoverable', important: true, emoji: '<:discoverable:850786735360966656>', weight: 6 },
+ ENABLED_DISCOVERABLE_BEFORE: {
+ name: 'Enabled Discovery Before',
+ important: false,
+ emoji: '<:enabledDiscoverableBefore:850786754670624828>',
+ weight: 7
+ },
+ FEATURABLE: { name: 'Featurable', important: true, emoji: '<:featurable:850786776372084756>', weight: 4 },
+ INVITE_SPLASH: { name: 'Invite Splash', important: false, emoji: '<:inviteSplash:850786798246559754>', weight: 16 },
+ MEMBER_VERIFICATION_GATE_ENABLED: {
+ name: 'Membership Verification Gate',
+ important: false,
+ emoji: '<:memberVerificationGateEnabled:850786829984858212>',
+ weight: 18
+ },
+ MONETIZATION_ENABLED: { name: 'Monetization Enabled', important: true, emoji: null, weight: 8 },
+ MORE_EMOJI: { name: 'More Emoji', important: true, emoji: '<:moreEmoji:850786853497602080>', weight: 3 },
+ MORE_STICKERS: { name: 'More Stickers', important: true, emoji: null, weight: 2 },
+ NEWS: {
+ name: 'Announcement Channels',
+ important: false,
+ emoji: '<:announcementChannels:850790491796013067>',
+ weight: 17
+ },
+ PARTNERED: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 },
+ PREVIEW_ENABLED: { name: 'Preview Enabled', important: true, emoji: '<:previewEnabled:850790508266913823>', weight: 10 },
+ RELAY_ENABLED: { name: 'Relay Enabled', important: true, emoji: '<:relayEnabled:850790531441229834>', weight: 5 },
+ TICKETED_EVENTS_ENABLED: { name: 'Ticketed Events Enabled', important: true, emoji: null, weight: 9 },
+ VANITY_URL: { name: 'Vanity URL', important: false, emoji: '<:vanityURL:850790553079644160>', weight: 12 },
+ VERIFIED: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 },
+ VIP_REGIONS: { name: 'VIP Regions', important: false, emoji: '<:VIPRegions:850794697496854538>', weight: 13 },
+ WELCOME_SCREEN_ENABLED: {
+ name: 'Welcome Screen Enabled',
+ important: false,
+ emoji: '<:welcomeScreenEnabled:850790575875817504>',
+ weight: 19
+ }
+ },
+
+ otherEmojis: {
+ SERVER_BOOSTER_1: '<:serverBooster1:848740052091142145>',
+ SERVER_BOOSTER_2: '<:serverBooster2:848740090506510388>',
+ SERVER_BOOSTER_3: '<:serverBooster3:848740124992077835>',
+ SERVER_BOOSTER_6: '<:serverBooster6:848740155245461514>',
+ SERVER_BOOSTER_9: '<:serverBooster9:848740188846030889>',
+ SERVER_BOOSTER_12: '<:serverBooster12:848740304365551668>',
+ SERVER_BOOSTER_15: '<:serverBooster15:848740354890137680>',
+ SERVER_BOOSTER_18: '<:serverBooster18:848740402886606868>',
+ SERVER_BOOSTER_24: '<:serverBooster24:848740444628320256>',
+ NITRO: '<:nitro:848740498054971432>',
+ BOOSTER: '<:booster:848747775020892200>',
+ OWNER: '<:owner:848746439311753286>',
+ ADMIN: '<:admin:848963914628333598>',
+ SUPERUSER: '<:superUser:848947986326224926>',
+ DEVELOPER: '<:developer:848954538111139871>',
+ BUSH_VERIFIED: '<:verfied:853360152090771497>',
+ BOOST_1: '<:boostitle:853363736679940127>',
+ BOOST_2: '<:boostitle:853363752728789075>',
+ BOOST_3: '<:boostitle:853363769132056627>',
+ TEXT: '<:text:853375537791893524>',
+ NEWS: '<:announcements:853375553531674644>',
+ VOICE: '<:voice:853375566735212584>',
+ STAGE: '<:stage:853375583521210468>',
+ STORE: '<:store:853375601175691266>',
+ CATEGORY: '<:category:853375615260819476>'
+ },
+
+ userFlags: {
+ DISCORD_EMPLOYEE: '<:discordEmployee:848742947826434079>',
+ PARTNERED_SERVER_OWNER: '<:partneredServerOwner:848743051593777152>',
+ HYPESQUAD_EVENTS: '<:hypeSquadEvents:848743108283072553>',
+ BUGHUNTER_LEVEL_1: '<:bugHunter:848743239850393640>',
+ HOUSE_BRAVERY: '<:hypeSquadBravery:848742910563844127>',
+ HOUSE_BRILLIANCE: '<:hypeSquadBrilliance:848742840649646101>',
+ HOUSE_BALANCE: '<:hypeSquadBalance:848742877537370133>',
+ EARLY_SUPPORTER: '<:earlySupporter:848741030102171648>',
+ //'TEAM_USER': '',
+ //'SYSTEM': '',
+ BUGHUNTER_LEVEL_2: '<:bugHunterGold:848743283080822794>',
+ //'VERIFIED_BOT': '',
+ EARLY_VERIFIED_BOT_DEVELOPER: '<:earlyVerifiedBotDeveloper:848741079875846174>'
+ },
+
+ status: {
+ online: '<:online:848937141639577690>',
+ idle: '<:idle:848937158261211146>',
+ dnd: '<:dnd:848937173780135986>',
+ offline: '<:offline:848939387277672448>',
+ streaming: '<:streaming:848937187479519242>'
+ },
+
+ maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'],
+
+ capes: [
+ // supporter capes
+ { name: 'patreon1', index: 0 },
+ { name: 'patreon2', index: 1 },
+ { name: 'fade', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/fade.gif', index: 2 },
+ { name: 'lava', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/lava.gif', index: 3 },
+ {
+ name: 'mcworld',
+ custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/mcworld_compressed.gif',
+ index: 4
+ },
+ {
+ name: 'negative',
+ custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/negative_compressed.gif',
+ index: 5
+ },
+ {
+ name: 'space',
+ custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/space_compressed.gif',
+ index: 6
+ },
+ { name: 'void', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/void.gif', index: 7 },
+ { name: 'tunnel', custom: 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/tunnel.gif', index: 8 },
+ // Staff capes
+ { name: 'contrib', index: 9 },
+ { name: 'mbstaff', index: 10 },
+ { name: 'ironmoon', index: 11 },
+ { name: 'gravy', index: 12 },
+ { name: 'nullzee', index: 13 },
+ // partner capes
+ { name: 'thebakery', index: 14 },
+ { name: 'dsm', index: 15 },
+ { name: 'packshq', index: 16 },
+ { name: 'furf', index: 17 },
+ { name: 'skytils', index: 18 },
+ { name: 'sbp', index: 19 },
+ { name: 'subreddit_light', index: 20 },
+ { name: 'subreddit_dark', index: 21 },
+ // streamer capes
+ { name: 'alexxoffi', index: 22 },
+ { name: 'jakethybro', index: 23 },
+ { name: 'krusty', index: 24 },
+ { name: 'soldier', index: 25 },
+ { name: 'zera', index: 26 }
+ ],
+ roleMap: [
+ { name: '*', id: '792453550768390194' },
+ { name: 'Admin Perms', id: '746541309853958186' },
+ { name: 'Sr. Moderator', id: '782803470205190164' },
+ { name: 'Moderator', id: '737308259823910992' },
+ { name: 'Helper', id: '737440116230062091' },
+ { name: 'Trial Helper', id: '783537091946479636' },
+ { name: 'Contributor', id: '694431057532944425' },
+ { name: 'Giveaway Donor', id: '784212110263451649' },
+ { name: 'Giveaway (200m)', id: '810267756426690601' },
+ { name: 'Giveaway (100m)', id: '801444430522613802' },
+ { name: 'Giveaway (50m)', id: '787497512981757982' },
+ { name: 'Giveaway (25m)', id: '787497515771232267' },
+ { name: 'Giveaway (10m)', id: '787497518241153025' },
+ { name: 'Giveaway (5m)', id: '787497519768403989' },
+ { name: 'Giveaway (1m)', id: '787497521084891166' },
+ { name: 'Suggester', id: '811922322767609877' },
+ { name: 'Partner', id: '767324547312779274' },
+ { name: 'Level Locked', id: '784248899044769792' },
+ { name: 'No Files', id: '786421005039173633' },
+ { name: 'No Reactions', id: '786421270924361789' },
+ { name: 'No Links', id: '786421269356740658' },
+ { name: 'No Bots', id: '786804858765312030' },
+ { name: 'No VC', id: '788850482554208267' },
+ { name: 'No Giveaways', id: '808265422334984203' },
+ { name: 'No Support', id: '790247359824396319' },
+ { name: 'DJ', id: '782619038403919902' }
+ ],
+ roleWhitelist: {
+ 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'],
+ 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'],
+ 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'DJ': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator']
+ }
+ };
+}
+
+// I just copy pasted this code from stackoverflow don't yell at me if there is issues for it
+export class CanvasProgressBar {
+ private x: number;
+ private y: number;
+ private w: number;
+ private h: number;
+ private color: string;
+ private percentage: number;
+ private p: number;
+ private ctx: CanvasRenderingContext2D;
+
+ constructor(
+ ctx: CanvasRenderingContext2D,
+ dimension: { x: number; y: number; width: number; height: number },
+ color: string,
+ percentage: number
+ ) {
+ ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension);
+ this.color = color;
+ this.percentage = percentage;
+ this.p;
+ this.ctx = ctx;
+ }
+
+ draw(): void {
+ // -----------------
+ this.p = this.percentage * this.w;
+ if (this.p <= this.h) {
+ this.ctx.beginPath();
+ this.ctx.arc(
+ this.h / 2 + this.x,
+ this.h / 2 + this.y,
+ this.h / 2,
+ Math.PI - Math.acos((this.h - this.p) / this.h),
+ Math.PI + Math.acos((this.h - this.p) / this.h)
+ );
+ this.ctx.save();
+ this.ctx.scale(-1, 1);
+ this.ctx.arc(
+ this.h / 2 - this.p - this.x,
+ this.h / 2 + this.y,
+ this.h / 2,
+ Math.PI - Math.acos((this.h - this.p) / this.h),
+ Math.PI + Math.acos((this.h - this.p) / this.h)
+ );
+ this.ctx.restore();
+ this.ctx.closePath();
+ } else {
+ this.ctx.beginPath();
+ this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI);
+ this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y);
+ this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2);
+ this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y);
+ this.ctx.closePath();
+ }
+ this.ctx.fillStyle = this.color;
+ this.ctx.fill();
+ }
+
+ // showWholeProgressBar(){
+ // this.ctx.beginPath();
+ // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI);
+ // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y);
+ // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2);
+ // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y);
+ // this.ctx.strokeStyle = '#000000';
+ // this.ctx.stroke();
+ // this.ctx.closePath();
+ // }
+
+ get PPercentage(): number {
+ return this.percentage * 100;
+ }
+
+ set PPercentage(x: number) {
+ this.percentage = x / 100;
+ }
+}
diff --git a/src/lib/extensions/BushCommand.ts b/src/lib/extensions/BushCommand.ts
index 2b34c69..9de2c95 100644
--- a/src/lib/extensions/BushCommand.ts
+++ b/src/lib/extensions/BushCommand.ts
@@ -1,8 +1,16 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/no-explicit-any */
import { Command, CommandOptions } from 'discord-akairo';
import { APIApplicationCommandOption } from 'discord-api-types';
+import { CommandInteraction, Snowflake } from 'discord.js';
import { BushClient } from './BushClient';
+import { BushInteractionMessage } from './BushInteractionMessage';
+import { BushMessage } from './BushMessage';
export interface BushCommandOptions extends CommandOptions {
+ hidden?: boolean;
+ restrictedChannels?: Snowflake[];
+ restrictedGuilds?: Snowflake[];
slashCommandOptions?: APIApplicationCommandOption[];
description: {
content: string;
@@ -12,10 +20,33 @@ export interface BushCommandOptions extends CommandOptions {
}
export class BushCommand extends Command {
- public client: BushClient;
- options: BushCommandOptions;
+ public declare client: BushClient;
+ public options: BushCommandOptions;
+ /** The channels the command is limited to run in. */
+ public restrictedChannels: Snowflake[];
+ /** The guilds the command is limited to run in. */
+ public restrictedGuilds: Snowflake[];
+ /** Whether the command is hidden from the help command. */
+ public hidden: boolean;
constructor(id: string, options?: BushCommandOptions) {
super(id, options);
this.options = options;
+ this.hidden = options.hidden || false;
+ this.restrictedChannels = options.restrictedChannels;
+ this.restrictedGuilds = options.restrictedGuilds;
+ }
+
+ public exec(message: BushMessage, args: any): any;
+ // @ts-ignore: They are close enough
+ public exec(message: BushMessage | BushInteractionMessage, args: any): any {
+ // @ts-ignore: They are close enough
+ super.exec(message, args);
+ }
+
+ /** Be careful when using this with a slash command since only the interaction is parsed as the message */
+ public before(message: BushMessage): any;
+ public before(message: BushMessage | CommandInteraction): any;
+ public before(message) {
+ super.before(message);
}
}
diff --git a/src/lib/extensions/BushCommandHandler.ts b/src/lib/extensions/BushCommandHandler.ts
index 6ef44d7..670125d 100644
--- a/src/lib/extensions/BushCommandHandler.ts
+++ b/src/lib/extensions/BushCommandHandler.ts
@@ -1,15 +1,279 @@
-import { CommandHandler, CommandHandlerOptions } from 'discord-akairo';
-import { Collection } from 'discord.js';
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { AkairoMessage, Category, CommandHandler, CommandHandlerOptions, CommandUtil, Util } from 'discord-akairo';
+import { Collection, CommandInteraction, GuildMember, Interaction } from 'discord.js';
import { BushClient } from './BushClient';
import { BushCommand } from './BushCommand';
+import { BushMessage } from './BushMessage';
-export interface BushCommandHandlerOptions extends CommandHandlerOptions {}
+export const ArgumentMatches = {
+ PHRASE: 'phrase',
+ FLAG: 'flag',
+ OPTION: 'option',
+ REST: 'rest',
+ SEPARATE: 'separate',
+ TEXT: 'text',
+ CONTENT: 'content',
+ REST_CONTENT: 'restContent',
+ NONE: 'none'
+};
+export const ArgumentTypes = {
+ STRING: 'string',
+ LOWERCASE: 'lowercase',
+ UPPERCASE: 'uppercase',
+ CHAR_CODES: 'charCodes',
+ NUMBER: 'number',
+ INTEGER: 'integer',
+ BIGINT: 'bigint',
+ EMOJINT: 'emojint',
+ URL: 'url',
+ DATE: 'date',
+ COLOR: 'color',
+ USER: 'user',
+ USERS: 'users',
+ MEMBER: 'member',
+ MEMBERS: 'members',
+ RELEVANT: 'relevant',
+ RELEVANTS: 'relevants',
+ CHANNEL: 'channel',
+ CHANNELS: 'channels',
+ TEXT_CHANNEL: 'textChannel',
+ TEXT_CHANNELS: 'textChannels',
+ VOICE_CHANNEL: 'voiceChannel',
+ VOICE_CHANNELS: 'voiceChannels',
+ CATEGORY_CHANNEL: 'categoryChannel',
+ CATEGORY_CHANNELS: 'categoryChannels',
+ NEWS_CHANNEL: 'newsChannel',
+ NEWS_CHANNELS: 'newsChannels',
+ STORE_CHANNEL: 'storeChannel',
+ STORE_CHANNELS: 'storeChannels',
+ ROLE: 'role',
+ ROLES: 'roles',
+ EMOJI: 'emoji',
+ EMOJIS: 'emojis',
+ GUILD: 'guild',
+ GUILDS: 'guilds',
+ MESSAGE: 'message',
+ GUILD_MESSAGE: 'guildMessage',
+ RELEVANT_MESSAGE: 'relevantMessage',
+ INVITE: 'invite',
+ MEMBER_MENTION: 'memberMention',
+ CHANNEL_MENTION: 'channelMention',
+ ROLE_MENTION: 'roleMention',
+ EMOJI_MENTION: 'emojiMention',
+ COMMAND_ALIAS: 'commandAlias',
+ COMMAND: 'command',
+ INHIBITOR: 'inhibitor',
+ LISTENER: 'listener'
+};
+
+export const blockedReasons = {
+ DM: 'dm',
+ BOT: 'bot',
+ GUILD: 'guild',
+ OWNER: 'owner',
+ CLIENT: 'client',
+ DISABLED: 'disabled',
+ SUPERUSER: 'superuser',
+ ROLE_BLACKLIST: 'roleBlacklist',
+ USER_BLACKLIST: 'userBlacklist',
+ RESTRICTED_GUILD: 'restrictedGuild',
+ CHANNEL_BLACKLIST: 'channelBlacklist',
+ RESTRICTED_CHANNEL: 'restrictedChannel'
+};
+
+export const CommandHandlerEvents = {
+ MESSAGE_BLOCKED: 'messageBlocked',
+ MESSAGE_INVALID: 'messageInvalid',
+ COMMAND_BLOCKED: 'commandBlocked',
+ COMMAND_STARTED: 'commandStarted',
+ COMMAND_FINISHED: 'commandFinished',
+ COMMAND_CANCELLED: 'commandCancelled',
+ COMMAND_LOCKED: 'commandLocked',
+ COMMAND_INVALID: 'commandInvalid',
+ COMMAND_LOCKED_NSFW: 'commandLockedNsfw',
+ MISSING_PERMISSIONS: 'missingPermissions',
+ COOLDOWN: 'cooldown',
+ IN_PROMPT: 'inPrompt',
+ ERROR: 'error',
+ SLASH_COMMAND_BLOCKED: 'slashCommandBlocked'
+};
+
+// A large amount of this code is copied from Akairo so that I can add custom checks to it.
export class BushCommandHandler extends CommandHandler {
- public constructor(client: BushClient, options: BushCommandHandlerOptions) {
+ public declare client: BushClient;
+ public declare modules: Collection<string, BushCommand>;
+ public declare categories: Collection<string, Category<string, BushCommand>>;
+ public constructor(client: BushClient, options: CommandHandlerOptions) {
super(client, options);
this.client = client;
}
- declare modules: Collection<string, BushCommand>;
+ async handleSlash(interaction: Interaction): Promise<boolean> {
+ if (!interaction.isCommand()) return false;
+
+ if (await this.runAllTypeInhibitors(interaction)) {
+ return false;
+ }
+
+ if (!interaction.guildID) {
+ this.emit('slashGuildOnly', interaction);
+ return false;
+ }
+ const command = this.findCommand(interaction.commandName) as BushCommand;
+ const before = command.before(interaction);
+ if (Util.isPromise(before)) await before;
+
+ if (!command) {
+ this.emit('slashNotFound', interaction);
+ return false;
+ }
+
+ if (command.ownerOnly && !this.client.isOwner(interaction.user)) {
+ this.emit('slashBlocked', interaction, command, 'owner');
+ return false;
+ }
+ if (command.superUserOnly && !this.client.isSuperUser(interaction.user)) {
+ this.emit('slashBlocked', interaction, command, 'superuser');
+ return false;
+ }
+
+ if (interaction.channel.type !== 'dm') {
+ const userPermissions = interaction.channel.permissionsFor(interaction.member as GuildMember).toArray();
+
+ if (command.userPermissions) {
+ const userMissingPermissions =
+ typeof command.userPermissions === 'object'
+ ? command.userPermissions.filter((p) => !userPermissions.includes(p))
+ : '';
+ if (command.userPermissions && command.userPermissions.length > 0 && userMissingPermissions.length > 0) {
+ this.emit('slashMissingPermissions', interaction, command, 'user', userMissingPermissions);
+ return false;
+ }
+ }
+
+ const clientPermissions = interaction.channel.permissionsFor(interaction.guild.me).toArray();
+
+ if (command.clientPermissions) {
+ const clientMissingPermissions = command.clientPermissions.filter((p) => !clientPermissions.includes(p));
+ if (command.clientPermissions && command.clientPermissions.length > 0 && clientMissingPermissions.length > 0) {
+ this.emit('slashMissingPermissions', interaction, command, 'client', clientMissingPermissions);
+ return false;
+ }
+ }
+ }
+
+ //@ts-ignore: Typings are wrong
+ if (this.runCooldowns(interaction, command)) {
+ return true;
+ }
+ const message = new AkairoMessage(this.client, interaction, {
+ slash: true,
+ replied: this.autoDefer || command.slashEphemeral
+ });
+
+ if (this.commandUtil) {
+ if (this.commandUtils.has(message.id)) {
+ message.util = this.commandUtils.get(message.id);
+ } else {
+ message.util = new CommandUtil(this, message);
+ this.commandUtils.set(message.id, message.util);
+ }
+ }
+
+ let parsed = await this.parseCommand(message);
+ if (!parsed.command) {
+ const overParsed = await this.parseCommandOverwrittenPrefixes(message);
+ if (overParsed.command || (parsed.prefix == null && overParsed.prefix != null)) {
+ parsed = overParsed;
+ }
+ }
+
+ if (this.commandUtil) {
+ message.util.parsed = parsed;
+ }
+
+ try {
+ if (this.autoDefer || command.slashEphemeral) {
+ await interaction.defer(command.slashEphemeral);
+ }
+ const convertedOptions = {};
+ for (const option of interaction.options.values()) {
+ if (option.member) convertedOptions[option.name] = option.member;
+ else if (option.channel) convertedOptions[option.name] = option.channel;
+ else if (option.role) convertedOptions[option.name] = option.role;
+ else convertedOptions[option.name] = option.value;
+ }
+ this.emit('slashStarted', interaction, command);
+
+ if (command.execSlash || this.execSlash) await command.execSlash(message, convertedOptions);
+ else await command.exec(message, convertedOptions);
+
+ return true;
+ } catch (err) {
+ this.emit('slashError', err, message, command);
+ return false;
+ }
+ }
+ public async runPostTypeInhibitors(message: BushMessage, command: BushCommand): Promise<boolean> {
+ if (command.ownerOnly && !message.client.isOwner(message.author)) {
+ super.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.OWNER);
+ return true;
+ }
+ if (command.superUserOnly && !(this.client.isSuperUser(message.author) || this.client.isOwner(message.author))) {
+ super.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.SUPERUSER);
+ return true;
+ }
+ if (command.channel === 'guild' && !message.guild) {
+ this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.GUILD);
+ return true;
+ }
+ if (command.channel === 'dm' && message.guild) {
+ this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.DM);
+ return true;
+ }
+ if (command.restrictedChannels?.length && message.channel) {
+ if (!command.restrictedChannels.includes(message.channel.id)) {
+ this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.RESTRICTED_CHANNEL);
+ return true;
+ }
+ }
+ if (command.restrictedGuilds?.length && message.guild) {
+ if (!command.restrictedGuilds.includes(message.guild.id)) {
+ this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, blockedReasons.RESTRICTED_GUILD);
+ return true;
+ }
+ }
+ if (await this.runPermissionChecks(message, command)) {
+ return true;
+ }
+ const reason = this.inhibitorHandler ? await this.inhibitorHandler.test('post', message, command) : null;
+ if (reason != null) {
+ this.emit(CommandHandlerEvents.COMMAND_BLOCKED, message, command, reason);
+ return true;
+ }
+ if (this.runCooldowns(message, command)) {
+ return true;
+ }
+ return false;
+ }
+ public async runCommand(message: BushMessage, command: BushCommand, args: unknown): Promise<void> {
+ if (command.typing) {
+ message.channel.startTyping();
+ }
+ try {
+ this.emit(CommandHandlerEvents.COMMAND_STARTED, message, command, args);
+ const ret = await command.exec(message, args);
+ this.emit(CommandHandlerEvents.COMMAND_FINISHED, message, command, args, ret);
+ } finally {
+ if (command.typing) {
+ message.channel.stopTyping();
+ }
+ }
+ }
+ public runAllTypeInhibitors(message: BushMessage): any;
+ public runAllTypeInhibitors(message: BushMessage | CommandInteraction): any;
+ public runAllTypeInhibitors(message): any {
+ super.runAllTypeInhibitors(message);
+ }
}
diff --git a/src/lib/extensions/BushInhibitor.ts b/src/lib/extensions/BushInhibitor.ts
index d9a9b68..85d6de8 100644
--- a/src/lib/extensions/BushInhibitor.ts
+++ b/src/lib/extensions/BushInhibitor.ts
@@ -2,5 +2,5 @@ import { Inhibitor } from 'discord-akairo';
import { BushClient } from './BushClient';
export class BushInhibitor extends Inhibitor {
- public client: BushClient;
+ public declare client: BushClient;
}
diff --git a/src/lib/extensions/BushInhinitorHandler.ts b/src/lib/extensions/BushInhinitorHandler.ts
new file mode 100644
index 0000000..2a947da
--- /dev/null
+++ b/src/lib/extensions/BushInhinitorHandler.ts
@@ -0,0 +1,6 @@
+import { InhibitorHandler } from 'discord-akairo';
+import { BushClient } from './BushClient';
+
+export class BushInhibitorHandler extends InhibitorHandler {
+ public declare client: BushClient;
+}
diff --git a/src/lib/extensions/BushListener.ts b/src/lib/extensions/BushListener.ts
index 6a13210..e555e89 100644
--- a/src/lib/extensions/BushListener.ts
+++ b/src/lib/extensions/BushListener.ts
@@ -2,5 +2,5 @@ import { Listener } from 'discord-akairo';
import { BushClient } from './BushClient';
export class BushListener extends Listener {
- public client: BushClient;
+ public declare client: BushClient;
}
diff --git a/src/lib/extensions/BushTask.ts b/src/lib/extensions/BushTask.ts
index 21655e9..06d0602 100644
--- a/src/lib/extensions/BushTask.ts
+++ b/src/lib/extensions/BushTask.ts
@@ -2,5 +2,5 @@ import { Task } from 'discord-akairo';
import { BushClient } from './BushClient';
export class BushTask extends Task {
- public client: BushClient;
+ public declare client: BushClient;
}
diff --git a/src/lib/extensions/BushTaskHandler.ts b/src/lib/extensions/BushTaskHandler.ts
index f783eb3..c76dbfb 100644
--- a/src/lib/extensions/BushTaskHandler.ts
+++ b/src/lib/extensions/BushTaskHandler.ts
@@ -1,6 +1,7 @@
import { AkairoHandlerOptions, TaskHandler } from 'discord-akairo';
import { BushClient } from './BushClient';
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface BushTaskHandlerOptions extends AkairoHandlerOptions {}
export class BushTaskHandler extends TaskHandler {
diff --git a/src/lib/extensions/Util.ts b/src/lib/extensions/Util.ts
deleted file mode 100644
index 3913437..0000000
--- a/src/lib/extensions/Util.ts
+++ /dev/null
@@ -1,598 +0,0 @@
-import chalk from 'chalk';
-import { exec } from 'child_process';
-import { ClientUtil, Command } from 'discord-akairo';
-import {
- APIInteractionDataResolvedChannel,
- APIInteractionDataResolvedGuildMember,
- APIRole,
- ApplicationCommandOptionType
-} from 'discord-api-types';
-import {
- ButtonInteraction,
- CommandInteractionOption,
- Constants,
- Guild,
- GuildChannel,
- GuildMember,
- MessageActionRow,
- MessageButton,
- MessageComponentInteraction,
- MessageEmbed,
- MessageOptions,
- Role,
- Snowflake,
- User,
- Util
-} from 'discord.js';
-import got from 'got';
-import { promisify } from 'util';
-import { BushClient } from './BushClient';
-import { BushMessage } from './BushMessage';
-
-interface hastebinRes {
- key: string;
-}
-
-export interface uuidRes {
- uuid: string;
- username: string;
- username_history?: { username: string }[] | null;
- textures: {
- custom: boolean;
- slim: boolean;
- skin: {
- url: string;
- data: string;
- };
- raw: {
- value: string;
- signature: string;
- };
- };
- created_at: string;
-}
-
-export interface SlashCommandOption<T> {
- name: string;
- type: ApplicationCommandOptionType;
- value?: T;
- options?: CommandInteractionOption[];
- user?: User;
- member?: GuildMember | APIInteractionDataResolvedGuildMember;
- channel?: GuildChannel | APIInteractionDataResolvedChannel;
- role?: Role | APIRole;
-}
-
-export class BushUtil extends ClientUtil {
- /**
- * The client of this ClientUtil
- * @type {BushClient}
- */
- public client: BushClient;
- /**
- * The hastebin urls used to post to hastebin, attempts to post in order
- * @type {string[]}
- */
- public hasteURLs = [
- 'https://hst.sh',
- 'https://hasteb.in',
- 'https://hastebin.com',
- 'https://mystb.in',
- 'https://haste.clicksminuteper.net',
- 'https://paste.pythondiscord.com',
- 'https://haste.unbelievaboat.com',
- 'https://haste.tyman.tech'
- ];
- /**
- * A simple promise exec method
- */
- private exec = promisify(exec);
-
- /**
- * Creates this client util
- * @param client The client to initialize with
- */
- constructor(client: BushClient) {
- super(client);
- }
-
- /**
- * Maps an array of user ids to user objects.
- * @param ids The list of IDs to map
- * @returns The list of users mapped
- */
- public async mapIDs(ids: Snowflake[]): Promise<User[]> {
- return await Promise.all(ids.map((id) => this.client.users.fetch(id)));
- }
-
- /**
- * Capitalizes the first letter of the given text
- * @param text The text to capitalize
- * @returns The capitalized text
- */
- public capitalize(text: string): string {
- return text.charAt(0).toUpperCase() + text.slice(1);
- }
-
- /**
- * Runs a shell command and gives the output
- * @param command The shell command to run
- * @returns The stdout and stderr of the shell command
- */
- public async shell(command: string): Promise<{
- stdout: string;
- stderr: string;
- }> {
- return await this.exec(command);
- }
-
- /**
- * Posts text to hastebin
- * @param content The text to post
- * @returns The url of the posted text
- */
- public async haste(content: string): Promise<string> {
- for (const url of this.hasteURLs) {
- try {
- const res: hastebinRes = await got.post(`${url}/documents`, { body: content }).json();
- return `${url}/${res.key}`;
- } catch (e) {
- // pass
- }
- }
- throw new Error('No urls worked. (wtf)');
- }
-
- /**
- * Resolves a user-provided string into a user object, if possible
- * @param text The text to try and resolve
- * @returns The user resolved or null
- */
- public async resolveUserAsync(text: string): Promise<User | null> {
- const idReg = /\d{17,19}/;
- const idMatch = text.match(idReg);
- if (idMatch) {
- try {
- const user = await this.client.users.fetch(text as Snowflake);
- return user;
- } catch {
- // pass
- }
- }
- const mentionReg = /<@!?(?<id>\d{17,19})>/;
- const mentionMatch = text.match(mentionReg);
- if (mentionMatch) {
- try {
- const user = await this.client.users.fetch(mentionMatch.groups.id as Snowflake);
- return user;
- } catch {
- // pass
- }
- }
- const user = this.client.users.cache.find((u) => u.username === text);
- if (user) return user;
- return null;
- }
-
- /**
- * Appends the correct ordinal to the given number
- * @param n The number to append an ordinal to
- * @returns The number with the ordinal
- */
- public ordinal(n: number): string {
- const s = ['th', 'st', 'nd', 'rd'],
- v = n % 100;
- return n + (s[(v - 20) % 10] || s[v] || s[0]);
- }
-
- /**
- * Chunks an array to the specified size
- * @param arr The array to chunk
- * @param perChunk The amount of items per chunk
- * @returns The chunked array
- */
- public chunk<T>(arr: T[], perChunk: number): T[][] {
- return arr.reduce((all, one, i) => {
- const ch = Math.floor(i / perChunk);
- all[ch] = [].concat(all[ch] || [], one);
- return all;
- }, []);
- }
-
- /**
- * The colors used throught the bot
- */
- public colors = {
- default: '#1FD8F1',
- error: '#EF4947',
- warn: '#FEBA12',
- success: '#3BB681',
- red: '#ff0000',
- orange: '#E86100',
- gold: '#b59400',
- yellow: '#ffff00',
- green: '#00ff1e',
- darkGreen: '#008f11',
- aqua: '#00bbff',
- blue: '#0055ff',
- blurple: '#5440cd',
- purple: '#8400ff',
- pink: '#ff00e6',
- white: '#ffffff',
- gray: '#a6a6a6',
- lightGray: '#cfcfcf',
- darkGray: '#7a7a7a',
- black: '#000000'
- };
-
- 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>'
- };
-
- /**
- * A simple utility to create and embed with the needed style for the bot
- */
- public createEmbed(color?: string, author?: User | GuildMember): MessageEmbed {
- if (author instanceof GuildMember) {
- author = author.user; // Convert to User if GuildMember
- }
- let embed = new MessageEmbed().setTimestamp();
- if (author)
- embed = embed.setAuthor(
- author.username,
- author.displayAvatarURL({ dynamic: true }),
- `https://discord.com/users/${author.id}`
- );
- if (color) embed = embed.setColor(color);
- return embed;
- }
-
- public async mcUUID(username: string): Promise<string> {
- const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as uuidRes;
- return apiRes.uuid.replace(/-/g, '');
- }
-
- public async syncSlashCommands(force = false, guild?: Snowflake): Promise<void> {
- let fetchedGuild: Guild;
- if (guild) fetchedGuild = this.client.guilds.cache.get(guild);
- try {
- const registered =
- guild === undefined ? await this.client.application.commands.fetch() : await fetchedGuild.commands.fetch();
- for (const [, registeredCommand] of registered) {
- if (!this.client.commandHandler.modules.find((cmd) => cmd.id == registeredCommand.name)?.execSlash || force) {
- guild === undefined
- ? await this.client.application.commands.delete(registeredCommand.id)
- : await fetchedGuild.commands.delete(registeredCommand.id);
- this.client.logger.verbose(
- chalk`{red Deleted slash command ${registeredCommand.name}${
- guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''
- }}`
- );
- }
- }
-
- for (const [, botCommand] of this.client.commandHandler.modules) {
- if (botCommand.execSlash) {
- const found = registered.find((i) => i.name == botCommand.id);
- Command;
- const slashdata = {
- name: botCommand.id,
- description: botCommand.description.content,
- options: botCommand.options.slashCommandOptions
- };
- botCommand;
-
- if (found?.id && !force) {
- if (slashdata.description !== found.description) {
- guild === undefined
- ? await this.client.application.commands.edit(found.id, slashdata)
- : fetchedGuild.commands.edit(found.id, slashdata);
- this.client.logger.verbose(
- chalk`{yellow Edited slash command ${botCommand.id}${
- guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''
- }}`
- );
- }
- } else {
- guild === undefined
- ? await this.client.application.commands.create(slashdata)
- : fetchedGuild.commands.create(slashdata);
- this.client.logger.verbose(
- chalk`{green Created slash command ${botCommand.id}${
- guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''
- }}`
- );
- }
- }
- }
-
- return this.client.logger.log(
- chalk.green(`Slash commands registered${guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''}`)
- );
- } catch (e) {
- console.log(chalk.red(e.stack));
- return this.client.logger.error(
- chalk`{red Slash commands not registered${
- guild !== undefined ? ` in guild ${fetchedGuild.name}` : ''
- }, see above error.}`
- );
- }
- }
-
- public moulberryBushRoleMap = [
- { name: '*', id: '792453550768390194' },
- { name: 'Admin Perms', id: '746541309853958186' },
- { name: 'Sr. Moderator', id: '782803470205190164' },
- { name: 'Moderator', id: '737308259823910992' },
- { name: 'Helper', id: '737440116230062091' },
- { name: 'Trial Helper', id: '783537091946479636' },
- { name: 'Contributor', id: '694431057532944425' },
- { name: 'Giveaway Donor', id: '784212110263451649' },
- { name: 'Giveaway (200m)', id: '810267756426690601' },
- { name: 'Giveaway (100m)', id: '801444430522613802' },
- { name: 'Giveaway (50m)', id: '787497512981757982' },
- { name: 'Giveaway (25m)', id: '787497515771232267' },
- { name: 'Giveaway (10m)', id: '787497518241153025' },
- { name: 'Giveaway (5m)', id: '787497519768403989' },
- { name: 'Giveaway (1m)', id: '787497521084891166' },
- { name: 'Suggester', id: '811922322767609877' },
- { name: 'Partner', id: '767324547312779274' },
- { name: 'Level Locked', id: '784248899044769792' },
- { name: 'No Files', id: '786421005039173633' },
- { name: 'No Reactions', id: '786421270924361789' },
- { name: 'No Links', id: '786421269356740658' },
- { name: 'No Bots', id: '786804858765312030' },
- { name: 'No VC', id: '788850482554208267' },
- { name: 'No Giveaways', id: '808265422334984203' },
- { name: 'No Support', id: '790247359824396319' }
- ];
-
- private paginateEmojis = {
- begging: '853667381335162910',
- back: '853667410203770881',
- stop: '853667471110570034',
- forward: '853667492680564747',
- end: '853667514915225640'
- };
-
- public async buttonPaginate(
- message: BushMessage,
- embeds: MessageEmbed[],
- text: string | null = null,
- deleteOnExit?: boolean
- ): Promise<void> {
- if (deleteOnExit === undefined) deleteOnExit = true;
- embeds.forEach((_e, i) => {
- embeds[i] = embeds[i].setFooter(`Page ${i + 1}/${embeds.length}`);
- });
-
- const style = Constants.MessageButtonStyles.PRIMARY;
- let curPage = 0;
- if (typeof embeds !== 'object') throw 'embeds must be an object';
- const msg = await message.util.reply({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] });
- const filter = (interaction: ButtonInteraction) =>
- interaction.customID.startsWith('paginate_') && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 });
- collector.on('collect', async (interaction: MessageComponentInteraction) => {
- if (interaction.user.id == message.author.id || message.client.config.owners.includes(interaction.user.id)) {
- switch (interaction.customID) {
- case 'paginate_beginning': {
- curPage = 0;
- await edit(interaction);
- break;
- }
- case 'paginate_back': {
- curPage--;
- await edit(interaction);
- break;
- }
- case 'paginate_stop': {
- if (deleteOnExit) {
- await interaction.deferUpdate();
- await msg.delete();
- } else {
- await interaction?.update({
- content: `${text ? text + '\n' : ''}Command closed by user.`,
- embeds: [],
- components: []
- });
- }
- return;
- }
- case 'paginate_next': {
- curPage++;
- await edit(interaction);
- break;
- }
- case 'paginate_end': {
- curPage = embeds.length - 1;
- await edit(interaction);
- break;
- }
- }
- } else {
- return await interaction?.deferUpdate();
- }
- });
-
- collector.on('end', async () => {
- await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => {});
- });
-
- async function edit(interaction: MessageComponentInteraction): Promise<void> {
- return await interaction?.update({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] });
- }
- function getPaginationRow(disableAll = false): MessageActionRow {
- return new MessageActionRow().addComponents(
- new MessageButton({
- style,
- customID: 'paginate_beginning',
- emoji: this.paginateEmojis.begging,
- disabled: disableAll || curPage == 0
- }),
- new MessageButton({
- style,
- customID: 'paginate_back',
- emoji: this.paginateEmojis.back,
- disabled: disableAll || curPage == 0
- }),
- new MessageButton({ style, customID: 'paginate_stop', emoji: this.paginateEmojis.stop, disabled: disableAll }),
- new MessageButton({
- style,
- customID: 'paginate_next',
- emoji: this.paginateEmojis.forward,
- disabled: disableAll || curPage == embeds.length - 1
- }),
- new MessageButton({
- style,
- customID: 'paginate_end',
- emoji: this.paginateEmojis.end,
- disabled: disableAll || curPage == embeds.length - 1
- })
- );
- }
- }
-
- public async sendWithDeleteButton(message: BushMessage, options: MessageOptions): Promise<void> {
- updateOptions();
- const msg = await message.util.reply(options as MessageOptions & { split?: false });
- const filter = (interaction: ButtonInteraction) => interaction.customID == 'paginate__stop' && interaction.message == msg;
- const collector = msg.createMessageComponentInteractionCollector(filter, { time: 300000 });
- collector.on('collect', async (interaction: MessageComponentInteraction) => {
- if (interaction.user.id == message.author.id || message.client.config.owners.includes(interaction.user.id)) {
- await interaction.deferUpdate();
- await msg.delete();
- return;
- } else {
- return await interaction?.deferUpdate();
- }
- });
-
- collector.on('end', async () => {
- updateOptions(true, true);
- await msg.edit(options);
- });
-
- function updateOptions(edit?: boolean, disable?: boolean) {
- if (edit == undefined) edit = false;
- if (disable == undefined) disable = false;
- options.components = [
- new MessageActionRow().addComponents(
- new MessageButton({
- style: Constants.MessageButtonStyles.PRIMARY,
- customID: 'paginate__stop',
- emoji: this.paginateEmojis.stop,
- disabled: disable
- })
- )
- ];
- if (edit) {
- options.reply = undefined;
- }
- }
- }
- /**
- * Surrounds text in a code block with the specified language and puts it in a haste bin if it too long.
- *
- * * Embed Description Limit = 2048 characters
- * * Embed Field Limit = 1024 characters
- */
- public async codeblock(code: string, length: number, language: 'ts' | 'js' | 'sh' | 'json' | '' = ''): Promise<string> {
- let hasteOut = '';
- const tildes = '```';
- const formattingLength = 2 * tildes.length + language.length + 2 * '\n'.length;
- if (code.length + formattingLength > length) hasteOut = 'Too large to display. Hastebin: ' + (await this.haste(code));
-
- const code2 = code.length > length ? code.substring(0, length - (hasteOut.length + '\n'.length + formattingLength)) : code;
- return (
- tildes + language + '\n' + Util.cleanCodeBlockContent(code2) + '\n' + tildes + (hasteOut.length ? '\n' + hasteOut : '')
- );
- }
-}
-
-// I just copy pasted this code from stackoverflow don't yell at me if there is issues for it
-export class CanvasProgressBar {
- private x: number;
- private y: number;
- private w: number;
- private h: number;
- private color: string;
- private percentage: number;
- private p: number;
- private ctx: CanvasRenderingContext2D;
-
- constructor(
- ctx: CanvasRenderingContext2D,
- dimension: { x: number; y: number; width: number; height: number },
- color: string,
- percentage: number
- ) {
- ({ x: this.x, y: this.y, width: this.w, height: this.h } = dimension);
- this.color = color;
- this.percentage = percentage;
- this.p;
- this.ctx = ctx;
- }
-
- draw() {
- // -----------------
- this.p = this.percentage * this.w;
- if (this.p <= this.h) {
- this.ctx.beginPath();
- this.ctx.arc(
- this.h / 2 + this.x,
- this.h / 2 + this.y,
- this.h / 2,
- Math.PI - Math.acos((this.h - this.p) / this.h),
- Math.PI + Math.acos((this.h - this.p) / this.h)
- );
- this.ctx.save();
- this.ctx.scale(-1, 1);
- this.ctx.arc(
- this.h / 2 - this.p - this.x,
- this.h / 2 + this.y,
- this.h / 2,
- Math.PI - Math.acos((this.h - this.p) / this.h),
- Math.PI + Math.acos((this.h - this.p) / this.h)
- );
- this.ctx.restore();
- this.ctx.closePath();
- } else {
- this.ctx.beginPath();
- this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, (3 / 2) * Math.PI);
- this.ctx.lineTo(this.p - this.h + this.x, 0 + this.y);
- this.ctx.arc(this.p - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, (3 / 2) * Math.PI, Math.PI / 2);
- this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y);
- this.ctx.closePath();
- }
- this.ctx.fillStyle = this.color;
- this.ctx.fill();
- }
-
- // showWholeProgressBar(){
- // this.ctx.beginPath();
- // this.ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI);
- // this.ctx.lineTo(this.w - this.h + this.x, 0 + this.y);
- // this.ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2);
- // this.ctx.lineTo(this.h / 2 + this.x, this.h + this.y);
- // this.ctx.strokeStyle = '#000000';
- // this.ctx.stroke();
- // this.ctx.closePath();
- // }
-
- get PPercentage() {
- return this.percentage * 100;
- }
-
- set PPercentage(x) {
- this.percentage = x / 100;
- }
-}
diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts
new file mode 100644
index 0000000..915fcb1
--- /dev/null
+++ b/src/lib/utils/BushCache.ts
@@ -0,0 +1,5 @@
+import { Snowflake } from 'discord.js';
+
+export class BushCache {
+ public static superUsers = new Array<Snowflake>();
+}
diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts
new file mode 100644
index 0000000..d48ec07
--- /dev/null
+++ b/src/lib/utils/BushLogger.ts
@@ -0,0 +1,191 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+import chalk from 'chalk';
+import { MessageEmbed } from 'discord.js';
+import { inspect } from 'util';
+import { BushClient, BushMessageType } from '../extensions/BushClient';
+
+export class BushLogger {
+ private client: BushClient;
+ public constructor(client: BushClient) {
+ this.client = client;
+ }
+
+ private parseFormatting(
+ content: any,
+ color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '',
+ discordFormat = false
+ ): string | typeof content {
+ if (typeof content !== 'string') return content;
+ const newContent: Array<string> = content.split(/<<|>>/);
+ const tempParsedArray: Array<string> = [];
+ newContent.forEach((value, index) => {
+ if (index % 2 !== 0) {
+ tempParsedArray.push(discordFormat ? `**${value}**` : chalk[color](value));
+ } else {
+ tempParsedArray.push(value);
+ }
+ });
+ return tempParsedArray.join('');
+ }
+
+ private inspectContent(content: any, depth = 2): string {
+ if (typeof content !== 'string') {
+ return inspect(content, { depth });
+ }
+ return content;
+ }
+
+ private stripColor(text: string): string {
+ return text.replace(
+ // eslint-disable-next-line no-control-regex
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
+ ''
+ );
+ }
+
+ private getTimeStamp(): string {
+ const now = new Date();
+ const hours = now.getHours();
+ const minute = now.getMinutes();
+ let hour = hours;
+ let amOrPm: 'AM' | 'PM' = 'AM';
+ if (hour > 12) {
+ amOrPm = 'PM';
+ hour = hour - 12;
+ }
+ return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}`;
+ }
+
+ /**
+ * Logs information. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header displayed before the content, displayed in cyan.
+ * @param content - The content to log, highlights displayed in bright blue.
+ * @param sendChannel - Should this also be logged to discord? Defaults to true.
+ */
+ public get log() {
+ return this.info;
+ }
+
+ /** Sends a message to the log channel */
+ public async channelLog(message: BushMessageType): Promise<void> {
+ const channel = await this.client.util.getConfigChannel('log');
+ await channel.send(message).catch(() => {});
+ }
+
+ /** Sends a message to the error channel */
+ public async channelError(message: BushMessageType): Promise<void> {
+ const channel = await this.client.util.getConfigChannel('error');
+ await channel.send(message).catch(() => {});
+ }
+
+ /**
+ * Logs debug information. Only works in dev is enabled in the config.
+ * @param content - The content to log.
+ */
+ public debug(...content: any): void {
+ if (!this.client.config.dev) return;
+ console.log(`${chalk.bgGrey(this.getTimeStamp())} ${chalk.grey('[Debug]')}`, ...content);
+ }
+
+ /**
+ * Logs verbose information. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header printed before the content, displayed in grey.
+ * @param content - The content to log, highlights displayed in bright black.
+ * @param sendChannel = false - Should this also be logged to discord? Defaults to false.
+ */
+ public async verbose(header: string, content: any, sendChannel = false): Promise<void> {
+ if (!this.client.config.logging.verbose) return;
+ const newContent = this.inspectContent(content);
+ console.info(
+ `${chalk.bgGrey(this.getTimeStamp())} ${chalk.grey(`[${header}]`)} ` + this.parseFormatting(newContent, 'blackBright')
+ );
+ if (!sendChannel) return;
+ const embed = new MessageEmbed()
+ .setDescription(`**[${header}]** ${this.stripColor(newContent)}`)
+ .setColor(this.client.util.colors.gray)
+ .setTimestamp();
+ this.channelLog({ embeds: [embed] });
+ }
+
+ /**
+ * Logs information. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header displayed before the content, displayed in cyan.
+ * @param content - The content to log, highlights displayed in bright blue.
+ * @param sendChannel - Should this also be logged to discord? Defaults to true.
+ */
+ public async info(header: string, content: any, sendChannel = true): Promise<void> {
+ if (!this.client.config.logging.info) return;
+ const newContent = this.inspectContent(content);
+ console.info(
+ `${chalk.bgCyan(this.getTimeStamp())} ${chalk.cyan(`[${header}]`)} ` + this.parseFormatting(newContent, 'blueBright')
+ );
+ if (!sendChannel) return;
+ const embed = new MessageEmbed()
+ .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`)
+ .setColor(this.client.util.colors.info)
+ .setTimestamp();
+ this.channelLog({ embeds: [embed] });
+ }
+
+ /**
+ * Logs warnings. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header displayed before the content, displayed in yellow.
+ * @param content - The content to log, highlights displayed in bright yellow.
+ * @param sendChannel - Should this also be logged to discord? Defaults to true.
+ */
+ public async warn(header: string, content: any, sendChannel = true): Promise<void> {
+ const newContent = this.inspectContent(content);
+ console.warn(
+ `${chalk.bgYellow(this.getTimeStamp())} ${chalk.yellow(`[${header}]`)} ` +
+ this.parseFormatting(newContent, 'yellowBright')
+ );
+
+ if (!sendChannel) return;
+ const embed = new MessageEmbed()
+ .setDescription(`**[${header}]** ${this.parseFormatting(this.stripColor(newContent), '', true)}`)
+ .setColor(this.client.util.colors.warn)
+ .setTimestamp();
+ this.channelLog({ embeds: [embed] });
+ }
+
+ /**
+ * Logs errors. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header displayed before the content, displayed in bright red.
+ * @param content - The content to log, highlights displayed in bright red.
+ * @param sendChannel - Should this also be logged to discord? Defaults to true.
+ */
+ public async error(header: string, content: any, sendChannel = true): Promise<void> {
+ const newContent = this.inspectContent(content);
+ console.error(
+ `${chalk.bgRedBright(this.getTimeStamp())} ${chalk.redBright(`[${header}]`)} ` +
+ this.parseFormatting(newContent, 'redBright')
+ );
+ if (!sendChannel) return;
+ const embed = new MessageEmbed()
+ .setDescription(`**[${header}]** ${this.stripColor(newContent)}`)
+ .setColor(this.client.util.colors.error)
+ .setTimestamp();
+ this.channelError({ embeds: [embed] });
+ }
+
+ /**
+ * Logs successes. Highlight information by surrounding it in `<<>>`.
+ * @param header - The header displayed before the content, displayed in green.
+ * @param content - The content to log, highlights displayed in bright green.
+ * @param sendChannel - Should this also be logged to discord? Defaults to true.
+ */
+ public async success(header: string, content: any, sendChannel = true): Promise<void> {
+ const newContent = this.inspectContent(content);
+ console.log(
+ `${chalk.bgGreen(this.getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ` +
+ this.parseFormatting(newContent, 'greenBright')
+ );
+ if (!sendChannel) return;
+ const embed = new MessageEmbed()
+ .setDescription(`**[${header}]** ${this.stripColor(newContent)}`)
+ .setColor(this.client.util.colors.success)
+ .setTimestamp();
+ await this.channelLog({ embeds: [embed] }).catch(() => {});
+ }
+}
diff --git a/src/lib/utils/Console.ts b/src/lib/utils/Console.ts
deleted file mode 100644
index a3bf326..0000000
--- a/src/lib/utils/Console.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import chalk from 'chalk';
-import { BushClient } from '../extensions/BushClient';
-
-export class Log {
- client: BushClient;
-
- public constructor(client: BushClient) {
- this.client = client;
- }
-
- private parseColors(
- content: any,
- color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright'
- ): string | any {
- if (typeof content === 'string') {
- const newContent: Array<string> = content.split(/<<|>>/);
- const tempParsedArray: Array<string> = [];
- newContent.forEach((value, index) => {
- if (index % 2 !== 0) {
- tempParsedArray.push(chalk[color](value));
- } else {
- tempParsedArray.push(value);
- }
- });
- return tempParsedArray.join('');
- } else {
- return content;
- }
- }
-
- private timeStamp(): string {
- const now = new Date();
- const hours = now.getHours();
- const minute = now.getMinutes();
- let hour = hours;
- let amOrPm: 'AM' | 'PM' = 'AM';
- if (hour > 12) {
- amOrPm = 'PM';
- hour = hour - 12;
- }
-
- return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}`;
- }
-
- /**
- * Logs debug information. Only works in dev is enabled in the config.
- * @param content - The content to log.
- */
- public debug(...content: any): void {
- if (this.client.config.dev) {
- console.log(`${chalk.bgGrey(this.timeStamp())} ${chalk.grey('[Debug]')}`, ...content);
- }
- }
-
- /**
- * Logs verbose information. Highlight information by surrounding it in `<<>>`.
- * @param header - The header displayed before the content, displayed in grey.
- * @param content - The content to log, highlights displayed in bright black.
- */
- public verbose(header: string, content: any): void {
- if (this.client.config.logging.verbose) {
- return console.info(
- `${chalk.bgGrey(this.timeStamp())} ${chalk.grey(`[${header}]`)} ` + this.parseColors(content, 'blackBright')
- );
- }
- }
-
- /**
- * Logs information. Highlight information by surrounding it in `<<>>`.
- * @param header - The header displayed before the content, displayed in cyan.
- * @param content - The content to log, highlights displayed in bright blue.
- */
- public info(header: string, content: any): void {
- if (this.client.config.logging.info) {
- return console.info(
- `${chalk.bgCyan(this.timeStamp())} ${chalk.cyan(`[${header}]`)} ` + this.parseColors(content, 'blueBright')
- );
- }
- }
-
- /**
- * Logs warnings. Highlight information by surrounding it in `<<>>`.
- * @param header - The header displayed before the content, displayed in yellow.
- * @param content - The content to log, highlights displayed in bright yellow.
- */
- public warn(header: string, content: any): void {
- return console.warn(
- `${chalk.bgYellow(this.timeStamp())} ${chalk.yellow(`[${header}]`)} ` + this.parseColors(content, 'yellowBright')
- );
- }
-
- /**
- * Logs errors. Highlight information by surrounding it in `<<>>`.
- * @param header - The header displayed before the content, displayed in bright red.
- * @param content - The content to log, highlights displayed in bright red.
- */
- public error(header: string, content: any): void {
- return console.error(
- `${chalk.bgRedBright(this.timeStamp())} ${chalk.redBright(`[${header}]`)} ` + this.parseColors(content, 'redBright')
- );
- }
-
- /**
- * Logs successes. Highlight information by surrounding it in `<<>>`.
- * @param header - The header displayed before the content, displayed in green.
- * @param content - The content to log, highlights displayed in bright green.
- */
- public success(header: string, content: any): void {
- return console.log(
- `${chalk.bgGreen(this.timeStamp())} ${chalk.greenBright(`[${header}]`)} ` + this.parseColors(content, 'greenBright')
- );
- }
-}
diff --git a/src/lib/utils/Logger.ts b/src/lib/utils/Logger.ts
deleted file mode 100644
index 0675e3d..0000000
--- a/src/lib/utils/Logger.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import chalk from 'chalk';
-import { TextChannel } from 'discord.js';
-import { BushClient } from '../extensions/BushClient';
-
-export class BushLogger {
- private client: BushClient;
- public constructor(client: BushClient) {
- this.client = client;
- }
- private stripColor(text: string): string {
- return text.replace(
- // eslint-disable-next-line no-control-regex
- /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
- ''
- );
- }
- public getChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> {
- return this.client.channels.fetch(this.client.config.channels[channel]) as Promise<TextChannel>;
- }
- public async log(message: string, sendChannel = false): Promise<void> {
- console.log(chalk`{bgCyan LOG} ` + message);
- if (sendChannel) {
- const channel = await this.getChannel('log');
- await channel.send('[LOG] ' + this.stripColor(message));
- }
- }
-
- public async verbose(message: string, sendChannel = false): Promise<void> {
- if (!this.client.config.logging.verbose) return;
- console.log(chalk`{bgMagenta VERBOSE} ` + message);
- if (sendChannel) {
- const channel = await this.getChannel('log');
- await channel.send('[VERBOSE] ' + this.stripColor(message));
- }
- }
- public async error(message: string, sendChannel = false): Promise<void> {
- console.log(chalk`{bgRed ERROR} ` + message);
- if (sendChannel) {
- const channel = await this.getChannel('error');
- await channel.send('[ERROR] ' + this.stripColor(message));
- }
- }
-}
diff --git a/src/listeners/client/ready.ts b/src/listeners/client/ready.ts
index d4b2808..a87d216 100644
--- a/src/listeners/client/ready.ts
+++ b/src/listeners/client/ready.ts
@@ -10,6 +10,25 @@ export default class ReadyListener extends BushListener {
}
public async exec(): Promise<void> {
- await this.client.logger.log(chalk`{green Sucessfully logged in as {cyan ${this.client.user.tag}}.}`, true);
+ //@ts-expect-error: ik its private, this is the only time I need to access it outside of its class
+ const timeStamp = chalk.bgGreen(this.client.logger.getTimeStamp()),
+ tag = chalk.magenta(this.client.user.tag),
+ guildCount = chalk.magenta(this.client.guilds.cache.size.toLocaleString()),
+ userCount = chalk.magenta(this.client.users.cache.size.toLocaleString());
+
+ console.log(`${timeStamp} Logged in to ${tag} serving ${guildCount} guilds and ${userCount} users.`);
+ console.log(
+ chalk.blue(`----------------------------------------------------------------------${this.client.config.dev ? '---' : ''}`)
+ );
+
+ this.client.user.setPresence({
+ activities: [
+ {
+ name: 'Beep Boop',
+ type: 'WATCHING'
+ }
+ ],
+ status: 'online'
+ });
}
}
diff --git a/src/listeners/commands/commandBlocked.ts b/src/listeners/commands/commandBlocked.ts
index 916f7cd..61433a6 100644
--- a/src/listeners/commands/commandBlocked.ts
+++ b/src/listeners/commands/commandBlocked.ts
@@ -1,6 +1,6 @@
-import { BushListener } from '../../lib/extensions/BushListener';
import { Command } from 'discord-akairo';
import { Message } from 'discord.js';
+import { BushListener } from '../../lib/extensions/BushListener';
export default class CommandBlockedListener extends BushListener {
public constructor() {
@@ -11,6 +11,12 @@ export default class CommandBlockedListener extends BushListener {
}
public async exec(message: Message, command: Command, reason: string): Promise<void> {
+ this.client.console.info(
+ 'CommandBlocked',
+ `<<${message.author.tag}>> tried to run <<${message.util.parsed.command}>> but was blocked because <<${reason}>>.`,
+ false
+ );
+
switch (reason) {
case 'owner': {
await message.util.send(`You must be an owner to run command \`${message.util.parsed.command}\``);
diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts
index 06aa55f..cb8c5d2 100644
--- a/src/listeners/commands/commandError.ts
+++ b/src/listeners/commands/commandError.ts
@@ -1,40 +1,65 @@
import { stripIndents } from 'common-tags';
-import { Message, MessageEmbed, TextChannel } from 'discord.js';
-import { BushCommand } from '../../lib/extensions/BushCommand';
+import { Command } from 'discord-akairo';
+import { MessageEmbed } from 'discord.js';
import { BushListener } from '../../lib/extensions/BushListener';
+import { BushMessage } from '../../lib/extensions/BushMessage';
export default class CommandErrorListener extends BushListener {
- constructor() {
- super('error', {
+ public constructor() {
+ super('commandError', {
emitter: 'commandHandler',
event: 'error'
});
}
- async exec(error: Error, message: Message, command?: BushCommand): Promise<void> {
- const errorNumber = Math.floor(Math.random() * 6969696969) + 69; // hehe funy numbers
- const errorDevEmbed = this.client.util
- .createEmbed(this.client.util.colors.error)
- .setTitle(`Error # \`${errorNumber}\`: An error occurred`)
+
+ public async exec(error: Error, message: BushMessage, command: Command | null | undefined): Promise<void> {
+ const errorNo = Math.floor(Math.random() * 6969696969) + 69; // hehe funny number
+ const errorEmbed: MessageEmbed = new MessageEmbed()
+ .setTitle(`Error # \`${errorNo}\`: An error occurred`)
.setDescription(
stripIndents`**User:** ${message.author} (${message.author.tag})
**Command:** ${command}
**Channel:** ${message.channel} (${message.channel.id})
**Message:** [link](${message.url})`
)
- .addField('Error', `${await this.client.util.haste(error.stack)}`);
- let errorUserEmbed: MessageEmbed;
- if (command) {
- errorUserEmbed = this.client.util
- .createEmbed(this.client.util.colors.error)
- .setTitle('An error occurred')
- .setDescription(
- stripIndents`Whoops! It appears like something broke.
- The developers have been notified of this. If you contact them, give them code \`${errorNumber}\`.
- `
- );
+ .addField('Error', await this.client.util.codeblock(`${error?.stack}`, 1024, 'js'))
+ .setColor(this.client.util.colors.error)
+ .setTimestamp();
+
+ if (message) {
+ if (!this.client.config.owners.includes(message.author.id)) {
+ const errorUserEmbed: MessageEmbed = new MessageEmbed()
+ .setTitle('An error occurred')
+ .setColor(this.client.util.colors.error)
+ .setTimestamp();
+ await this.client.logger.channelError({ embeds: [errorEmbed] });
+ if (!command)
+ errorUserEmbed.setDescription(`Oh no! An error occurred. Please give the developers code \`${errorNo}\`.`);
+ else
+ errorUserEmbed.setDescription(
+ `Oh no! While running the command \`${command.id}\`, an error occurred. Please give the developers code \`${errorNo}\`.`
+ );
+ await message.util.send({ embeds: [errorUserEmbed] }).catch((e) => {
+ const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name;
+ this.client.console.warn('CommandError', `Failed to send user error embed in <<${channel}>>:\n` + e?.stack);
+ });
+ } else {
+ const errorDevEmbed = new MessageEmbed()
+ .setTitle('An error occurred')
+ .setColor(this.client.util.colors.error)
+ .setTimestamp()
+ .setDescription(await this.client.util.codeblock(`${error?.stack}`, 2048, 'js'));
+ await message.util.send({ embeds: [errorDevEmbed] }).catch((e) => {
+ const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name;
+ this.client.console.warn('CommandError', `Failed to send owner error stack in <<${channel}>>.` + e?.stack);
+ });
+ }
}
- const channel = (await this.client.channels.fetch(this.client.config.channels.log)) as TextChannel;
- await channel.send({ embeds: [errorDevEmbed] });
- if (errorUserEmbed) await message.reply({ embeds: [errorUserEmbed] });
+ const channel = message.channel.type === 'dm' ? message.channel.recipient.tag : message.channel.name;
+ this.client.console.error(
+ 'CommandError',
+ `an error occurred with the <<${command}>> command in <<${channel}>> triggered by <<${message?.author?.tag}>>:\n` +
+ error?.stack
+ );
}
}
diff --git a/src/listeners/commands/commandStarted.ts b/src/listeners/commands/commandStarted.ts
index 1c5b0c7..28ed0f8 100644
--- a/src/listeners/commands/commandStarted.ts
+++ b/src/listeners/commands/commandStarted.ts
@@ -1,5 +1,4 @@
-import chalk from 'chalk';
-import { Message, DMChannel } from 'discord.js';
+import { Message } from 'discord.js';
import { BushCommand } from '../../lib/extensions/BushCommand';
import { BushListener } from '../../lib/extensions/BushListener';
@@ -11,10 +10,12 @@ export default class CommandStartedListener extends BushListener {
});
}
exec(message: Message, command: BushCommand): void {
- this.client.logger.verbose(
- chalk`{cyan {green ${message.author.tag}} is running {green ${command.aliases[0]}} in {green ${
- message.channel instanceof DMChannel ? 'DMs' : `#${message.channel.name} (Server: ${message.guild.name})`
- }}.}`
+ this.client.logger.info(
+ 'Command',
+ `The <<${command.id}>> command was used by <<${message.author.tag}>> in ${
+ message.channel.type === 'dm' ? `their <<DMs>>` : `<<#${message.channel.name}>> in <<${message.guild?.name}>>`
+ }.`,
+ false // I don't want to spam the log channel when people use commands
);
}
}
diff --git a/src/listeners/message/level.ts b/src/listeners/message/level.ts
index 1e93055..615c013 100644
--- a/src/listeners/message/level.ts
+++ b/src/listeners/message/level.ts
@@ -1,4 +1,3 @@
-import chalk from 'chalk';
import { Message } from 'discord.js';
import { BushListener } from '../../lib/extensions/BushListener';
import { Level } from '../../lib/models';
@@ -30,7 +29,7 @@ export default class LevelListener extends BushListener {
const xpToGive = Level.genRandomizedXp();
user.xp += xpToGive;
await user.save();
- await this.client.logger.verbose(chalk`{cyan Gave XP to {green ${message.author.tag}}: {green ${xpToGive}xp}.}`);
+ await this.client.logger.verbose(`LevelListener`, `Gave <<${xpToGive}>> XP to <<${message.author.tag}>>.`);
this.levelCooldowns.add(message.author.id);
setTimeout(() => this.levelCooldowns.delete(message.author.id), 60_000);
}
diff --git a/src/listeners/other/consoleListener.ts b/src/listeners/other/consoleListener.ts
new file mode 100644
index 0000000..50c0cf3
--- /dev/null
+++ b/src/listeners/other/consoleListener.ts
@@ -0,0 +1,50 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { BushListener } from '../../lib/extensions/BushListener';
+
+export default class ConsoleListener extends BushListener {
+ public constructor() {
+ super('console', {
+ emitter: 'stdin',
+ event: 'line'
+ });
+ }
+
+ public async exec(line: string): Promise<void> {
+ const bot = this.client;
+ if (line.startsWith('eval ')) {
+ try {
+ const input = line.replace('eval ', '');
+ let output = eval(input);
+ output = await output;
+ console.log(output);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ if (line.startsWith('ev ')) {
+ try {
+ const input = line.replace('ev ', '');
+ let output = eval(input);
+ output = await output;
+ console.log(output);
+ } catch (e) {
+ console.error(e);
+ }
+ } /* else if (line.startsWith('reload')) {
+ exec('npx tsc', (error) => {
+ if (error) {
+ return this.client.console.error('Reload', `Error recompiling, \`${error.message}\``);
+ }
+ try {
+ this.client.commandHandler.reloadAll();
+ this.client.listenerHandler.reloadAll();
+ } catch (e) {
+ return this.client.console.error('Reload', e);
+ }
+ this.client.console.success('Reload', 'Reloaded successfully.');
+ });
+ } else if (line.startsWith('stop') || line.startsWith('exit')) {
+ process.exit();
+ } */
+ }
+}
diff --git a/src/listeners/other/promiseRejection.ts b/src/listeners/other/promiseRejection.ts
new file mode 100644
index 0000000..2d7e316
--- /dev/null
+++ b/src/listeners/other/promiseRejection.ts
@@ -0,0 +1,23 @@
+import { BushListener } from '../../lib/extensions/BushListener';
+
+export default class PromiseRejectionListener extends BushListener {
+ constructor() {
+ super('promiseRejection', {
+ emitter: 'process',
+ event: 'unhandledRejection'
+ });
+ }
+
+ public async exec(error: Error): Promise<void> {
+ this.client.console.error('PromiseRejection', 'An unhanded promise rejection occurred:\n' + error.stack, false);
+ await this.client.console.channelError({
+ embeds: [
+ {
+ title: 'Unhandled promise rejection',
+ fields: [{ name: 'error', value: await this.client.util.codeblock(error.stack, 1024, 'js') }],
+ color: this.client.util.colors.error
+ }
+ ]
+ });
+ }
+}
diff --git a/src/tasks/unban.ts b/src/tasks/unban.ts
index 564a2a3..6e06db8 100644
--- a/src/tasks/unban.ts
+++ b/src/tasks/unban.ts
@@ -1,4 +1,3 @@
-import chalk from 'chalk';
import { DiscordAPIError } from 'discord.js';
import { Op } from 'sequelize';
import { BushTask } from '../lib/extensions/BushTask';
@@ -23,7 +22,7 @@ export default class UnbanTask extends BushTask {
]
}
});
- this.client.logger.verbose(chalk.cyan(`Queried bans, found ${rows.length} expired bans.`));
+ this.client.logger.verbose(`UnbanTask`, `Queried bans, found <<${rows.length}>> expired bans.`);
for (const row of rows) {
const guild = this.client.guilds.cache.get(row.guild);
if (!guild) {
@@ -38,7 +37,7 @@ export default class UnbanTask extends BushTask {
} else throw e;
}
await row.destroy();
- this.client.logger.verbose(chalk.cyan('Unbanned user'));
+ this.client.logger.verbose(`UnbanTask`, `Unbanned ${row.user}`);
}
}
}