From ea64ebfff9aae32deb036643422d3427959dcd24 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Sat, 19 Jun 2021 16:43:37 -0400 Subject: feat(*): A bunch of stuff - Remade logging - updated dependencies - started adding custom crap to the command handler - added emojis to stuff - can't remeber other stuff Note: this is currently broken BREAKING CHANGE: --- src/commands/config/prefix.ts | 69 +++ src/commands/dev/eval.ts | 144 +++-- src/commands/dev/reload.ts | 4 +- src/commands/dev/setLevel.ts | 2 +- src/commands/info/help.ts | 2 +- src/commands/info/ping.ts | 2 +- src/commands/info/pronouns.ts | 2 +- src/commands/moderation/ban.ts | 12 +- src/commands/moderation/kick.ts | 10 +- src/commands/moderation/role.ts | 22 +- src/commands/moulberry-bush/capePerms.ts | 13 +- src/commands/moulberry-bush/giveawayPing.ts | 7 +- src/commands/moulberry-bush/rule.ts | 8 +- src/commands/server-config/prefix.ts | 69 --- src/config/example-options.ts | 4 +- src/lib/extensions/BushClient.ts | 78 ++- src/lib/extensions/BushClientUtil.ts | 879 ++++++++++++++++++++++++++++ src/lib/extensions/BushCommand.ts | 35 +- src/lib/extensions/BushCommandHandler.ts | 274 ++++++++- src/lib/extensions/BushInhibitor.ts | 2 +- src/lib/extensions/BushInhinitorHandler.ts | 6 + src/lib/extensions/BushListener.ts | 2 +- src/lib/extensions/BushTask.ts | 2 +- src/lib/extensions/BushTaskHandler.ts | 1 + src/lib/extensions/Util.ts | 598 ------------------- src/lib/utils/BushCache.ts | 5 + src/lib/utils/BushLogger.ts | 191 ++++++ src/lib/utils/Console.ts | 115 ---- src/lib/utils/Logger.ts | 43 -- src/listeners/client/ready.ts | 21 +- src/listeners/commands/commandBlocked.ts | 8 +- src/listeners/commands/commandError.ts | 71 ++- src/listeners/commands/commandStarted.ts | 13 +- src/listeners/message/level.ts | 3 +- src/listeners/other/consoleListener.ts | 50 ++ src/listeners/other/promiseRejection.ts | 23 + src/tasks/unban.ts | 5 +- 37 files changed, 1802 insertions(+), 993 deletions(-) create mode 100644 src/commands/config/prefix.ts delete mode 100644 src/commands/server-config/prefix.ts create mode 100644 src/lib/extensions/BushClientUtil.ts create mode 100644 src/lib/extensions/BushInhinitorHandler.ts delete mode 100644 src/lib/extensions/Util.ts create mode 100644 src/lib/utils/BushCache.ts create mode 100644 src/lib/utils/BushLogger.ts delete mode 100644 src/lib/utils/Console.ts delete mode 100644 src/lib/utils/Logger.ts create mode 100644 src/listeners/other/consoleListener.ts create mode 100644 src/listeners/other/promiseRejection.ts (limited to 'src') diff --git a/src/commands/config/prefix.ts b/src/commands/config/prefix.ts new file mode 100644 index 0000000..c20cfa5 --- /dev/null +++ b/src/commands/config/prefix.ts @@ -0,0 +1,69 @@ +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 { Guild } from '../../lib/models'; + +export default class PrefixCommand extends BushCommand { + constructor() { + super('prefix', { + aliases: ['prefix'], + category: 'config', + args: [ + { + id: 'prefix' + } + ], + userPermissions: ['MANAGE_GUILD'], + description: { + content: 'Set the prefix of the current server (resets to default if prefix is not given)', + usage: 'prefix [prefix]', + examples: ['prefix', 'prefix +'] + }, + slashCommandOptions: [ + { + type: ApplicationCommandOptionType.STRING, + name: 'prefix', + description: 'The prefix to set for this server', + required: false + } + ] + }); + } + + async changePrefix(guild: DiscordGuild, prefix?: string): Promise { + let row = await Guild.findByPk(guild.id); + if (!row) { + row = Guild.build({ + id: guild.id + }); + } + if (prefix) { + row.prefix = prefix; + await row.save(); + } else { + const row = await Guild.findByPk(guild.id); + row.prefix = this.client.config.prefix; + await row.save(); + } + } + + async exec(message: Message, { prefix }: { prefix?: string }): Promise { + await this.changePrefix(message.guild, prefix); + if (prefix) { + await message.util.send(`Sucessfully set prefix to \`${prefix}\``); + } else { + await message.util.send(`Sucessfully reset prefix to \`${this.client.config.prefix}\``); + } + } + + async execSlash(message: BushInteractionMessage, { prefix }: { prefix?: SlashCommandOption }): Promise { + await this.changePrefix(message.guild, prefix?.value); + if (prefix) { + await message.reply(`Sucessfully set prefix to \`${prefix.value}\``); + } else { + await message.reply(`Sucessfully reset prefix to \`${this.client.config.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 { 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 { - 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 = { @@ -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 { 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 { 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/commands/server-config/prefix.ts b/src/commands/server-config/prefix.ts deleted file mode 100644 index 9cdc331..0000000 --- a/src/commands/server-config/prefix.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { ApplicationCommandOptionType } from 'discord-api-types'; -import { Guild as DiscordGuild, Message } from 'discord.js'; -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', - args: [ - { - id: 'prefix' - } - ], - userPermissions: ['MANAGE_GUILD'], - description: { - content: 'Set the prefix of the current server (resets to default if prefix is not given)', - usage: 'prefix [prefix]', - examples: ['prefix', 'prefix +'] - }, - slashCommandOptions: [ - { - type: ApplicationCommandOptionType.STRING, - name: 'prefix', - description: 'The prefix to set for this server', - required: false - } - ] - }); - } - - async changePrefix(guild: DiscordGuild, prefix?: string): Promise { - let row = await Guild.findByPk(guild.id); - if (!row) { - row = Guild.build({ - id: guild.id - }); - } - if (prefix) { - row.prefix = prefix; - await row.save(); - } else { - const row = await Guild.findByPk(guild.id); - row.prefix = this.client.config.prefix; - await row.save(); - } - } - - async exec(message: Message, { prefix }: { prefix?: string }): Promise { - await this.changePrefix(message.guild, prefix); - if (prefix) { - await message.util.send(`Sucessfully set prefix to \`${prefix}\``); - } else { - await message.util.send(`Sucessfully reset prefix to \`${this.client.config.prefix}\``); - } - } - - async execSlash(message: BushInteractionMessage, { prefix }: { prefix?: SlashCommandOption }): Promise { - await this.changePrefix(message.guild, prefix?.value); - if (prefix) { - await message.reply(`Sucessfully set prefix to \`${prefix.value}\``); - } else { - await message.reply(`Sucessfully reset prefix to \`${this.client.config.prefix}\``); - } - } -} 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 { 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 { - 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 { @@ -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 { + 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 { + 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 { + 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 { + 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 = /<@!?(?\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(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 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.client.channels.fetch(this.client.config.channels[channel]) as Promise; + } + + /** 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