aboutsummaryrefslogtreecommitdiff
path: root/src/lib/extensions/discord-akairo
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-06-29 20:48:27 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2021-06-29 20:48:27 -0400
commitfd675ca9d60cc06d892ebc36a1b9624f15233f20 (patch)
tree0c510afb64eb2c6f45c15c7e22148c6e7f7ed2d7 /src/lib/extensions/discord-akairo
parent921b07f9716fdd413ec89c4a40419a6e2d39824b (diff)
downloadtanzanite-fd675ca9d60cc06d892ebc36a1b9624f15233f20.tar.gz
tanzanite-fd675ca9d60cc06d892ebc36a1b9624f15233f20.tar.bz2
tanzanite-fd675ca9d60cc06d892ebc36a1b9624f15233f20.zip
don't judge part 1
Diffstat (limited to 'src/lib/extensions/discord-akairo')
-rw-r--r--src/lib/extensions/discord-akairo/BushArgumentOptions.ts59
-rw-r--r--src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts4
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts233
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts536
-rw-r--r--src/lib/extensions/discord-akairo/BushCommand.ts57
-rw-r--r--src/lib/extensions/discord-akairo/BushCommandHandler.ts90
-rw-r--r--src/lib/extensions/discord-akairo/BushCommandUtil.ts10
-rw-r--r--src/lib/extensions/discord-akairo/BushInhibitor.ts15
-rw-r--r--src/lib/extensions/discord-akairo/BushInhinitorHandler.ts6
-rw-r--r--src/lib/extensions/discord-akairo/BushListener.ts6
-rw-r--r--src/lib/extensions/discord-akairo/BushListenerHandler.ts6
-rw-r--r--src/lib/extensions/discord-akairo/BushSlashMessage.ts20
-rw-r--r--src/lib/extensions/discord-akairo/BushTask.ts6
-rw-r--r--src/lib/extensions/discord-akairo/BushTaskHandler.ts11
14 files changed, 1059 insertions, 0 deletions
diff --git a/src/lib/extensions/discord-akairo/BushArgumentOptions.ts b/src/lib/extensions/discord-akairo/BushArgumentOptions.ts
new file mode 100644
index 0000000..bbbc04b
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushArgumentOptions.ts
@@ -0,0 +1,59 @@
+import { ArgumentOptions, ArgumentTypeCaster } from 'discord-akairo';
+
+type BushArgumentType =
+ | 'string'
+ | 'lowercase'
+ | 'uppercase'
+ | 'charCodes'
+ | 'number'
+ | 'integer'
+ | 'bigint'
+ | 'emojint'
+ | 'url'
+ | 'date'
+ | 'color'
+ | 'user'
+ | 'users'
+ | 'member'
+ | 'members'
+ | 'relevant'
+ | 'relevants'
+ | 'channel'
+ | 'channels'
+ | 'textChannel'
+ | 'textChannels'
+ | 'voiceChannel'
+ | 'voiceChannels'
+ | 'categoryChannel'
+ | 'categoryChannels'
+ | 'newsChannel'
+ | 'newsChannels'
+ | 'storeChannel'
+ | 'storeChannels'
+ | 'role'
+ | 'roles'
+ | 'emoji'
+ | 'emojis'
+ | 'guild'
+ | 'guilds'
+ | 'message'
+ | 'guildMessage'
+ | 'relevantMessage'
+ | 'invite'
+ | 'userMention'
+ | 'memberMention'
+ | 'channelMention'
+ | 'roleMention'
+ | 'emojiMention'
+ | 'commandAlias'
+ | 'command'
+ | 'inhibitor'
+ | 'listener'
+ | 'duration'
+ | (string | string[])[]
+ | RegExp
+ | string;
+
+export interface BushArgumentOptions extends ArgumentOptions {
+ type?: BushArgumentType | ArgumentTypeCaster;
+}
diff --git a/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts b/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
new file mode 100644
index 0000000..9afcf8b
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushArgumentTypeCaster.ts
@@ -0,0 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { BushMessage } from '../discord.js/BushMessage';
+
+export type BushArgumentTypeCaster = (message: BushMessage, phrase: string) => any;
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
new file mode 100644
index 0000000..c18fe80
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -0,0 +1,233 @@
+import chalk from 'chalk';
+import { AkairoClient } from 'discord-akairo';
+import {
+ Guild,
+ Intents,
+ Message,
+ MessageEditOptions,
+ MessageOptions,
+ MessagePayload,
+ ReplyMessageOptions,
+ Snowflake,
+ Structures,
+ UserResolvable
+} from 'discord.js';
+import * as path from 'path';
+import { exit } from 'process';
+import readline from 'readline';
+import { Sequelize } from 'sequelize';
+import { durationTypeCaster } from '../../../arguments/duration';
+import * as config from '../../../config/options';
+import UpdateCacheTask from '../../../tasks/updateCache';
+import * as Models from '../../models';
+import AllowedMentions from '../../utils/AllowedMentions';
+import { BushCache } from '../../utils/BushCache';
+import { BushConstants } from '../../utils/BushConstants';
+import { BushLogger } from '../../utils/BushLogger';
+import { BushGuild } from '../discord.js/BushGuild';
+import { BushGuildMember } from '../discord.js/BushGuildMember';
+import { BushMessage } from '../discord.js/BushMessage';
+import { BushUser } from '../discord.js/BushUser';
+import { BushClientUtil } from './BushClientUtil';
+import { BushCommandHandler } from './BushCommandHandler';
+import { BushInhibitorHandler } from './BushInhinitorHandler';
+import { BushListenerHandler } from './BushListenerHandler';
+import { BushTaskHandler } from './BushTaskHandler';
+
+export type BotConfig = typeof config;
+export type BushReplyMessageType = string | MessagePayload | ReplyMessageOptions;
+export type BushEditMessageType = string | MessageEditOptions | MessagePayload;
+export type BushSendMessageType = string | MessagePayload | MessageOptions;
+
+const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: false
+});
+
+export class BushClient extends AkairoClient {
+ public config: BotConfig;
+ public listenerHandler: BushListenerHandler;
+ public inhibitorHandler: BushInhibitorHandler;
+ public commandHandler: BushCommandHandler;
+ public taskHandler: BushTaskHandler;
+ public declare util: BushClientUtil;
+ public declare ownerID: Snowflake[];
+ public db: Sequelize;
+ public logger: BushLogger;
+ public constants = BushConstants;
+ public cache = BushCache;
+ constructor(config: BotConfig) {
+ super(
+ {
+ ownerID: config.owners,
+ intents: Intents.ALL
+ },
+ {
+ allowedMentions: AllowedMentions.users(), // No everyone or role mentions by default
+ intents: Intents.ALL
+ }
+ );
+
+ // Set token
+ this.token = config.credentials.token;
+
+ // Set config
+ this.config = config;
+
+ // Create listener handler
+ this.listenerHandler = new BushListenerHandler(this, {
+ directory: path.join(__dirname, '..', '..', 'listeners'),
+ automateCategories: true
+ });
+
+ // Create inhibitor handler
+ this.inhibitorHandler = new BushInhibitorHandler(this, {
+ directory: path.join(__dirname, '..', '..', 'inhibitors'),
+ automateCategories: true
+ });
+
+ // Create task handler
+ this.taskHandler = new BushTaskHandler(this, {
+ directory: path.join(__dirname, '..', '..', 'tasks')
+ });
+
+ // Create command handler
+ this.commandHandler = new BushCommandHandler(this, {
+ directory: path.join(__dirname, '..', '..', 'commands'),
+ prefix: async ({ guild }: { guild: Guild }) => {
+ if (this.config.dev) return 'dev ';
+ const row = await Models.Guild.findByPk(guild.id);
+ return (row?.prefix || this.config.prefix) as string;
+ },
+ allowMention: true,
+ handleEdits: true,
+ commandUtil: true,
+ commandUtilLifetime: 300_000,
+ argumentDefaults: {
+ prompt: {
+ 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: ''
+ },
+
+ automateCategories: false,
+ autoRegisterSlashCommands: true
+ });
+
+ 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,
+ port: this.config.db.port,
+ logging: this.config.logging.db ? (a) => this.logger.debug(a) : false
+ });
+ this.logger = new BushLogger(this);
+ }
+
+ get console(): BushLogger {
+ return this.logger;
+ }
+
+ get consts(): typeof BushConstants {
+ return this.constants;
+ }
+
+ // Initialize everything
+ private async _init(): Promise<void> {
+ Structures.extend('User', () => BushUser);
+ Structures.extend('Guild', () => BushGuild);
+ Structures.extend('GuildMember', () => BushGuildMember);
+ Structures.extend('Message', () => BushMessage);
+
+ this.commandHandler.useListenerHandler(this.listenerHandler);
+ this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
+ this.commandHandler.ignorePermissions = this.config.owners;
+ this.commandHandler.ignoreCooldown = this.config.owners.concat(this.cache.global.superUsers);
+ this.listenerHandler.setEmitters({
+ client: this,
+ commandHandler: this.commandHandler,
+ listenerHandler: this.listenerHandler,
+ inhibitorHandler: this.inhibitorHandler,
+ taskHandler: this.taskHandler,
+ process,
+ stdin: rl,
+ gateway: this.ws
+ });
+ this.commandHandler.resolver.addTypes({
+ duration: durationTypeCaster
+ });
+ // loads all the handlers
+ const loaders = {
+ commands: this.commandHandler,
+ listeners: this.listenerHandler,
+ inhibitors: this.inhibitorHandler,
+ tasks: this.taskHandler
+ };
+ for (const loader of Object.keys(loaders)) {
+ try {
+ loaders[loader].loadAll();
+ await this.logger.success('Startup', `Successfully loaded <<${loader}>>.`, false);
+ } catch (e) {
+ await this.logger.error('Startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack}`, false);
+ }
+ }
+ await this.dbPreInit();
+ await new UpdateCacheTask().init(this);
+ this.console.success('Startup', `Successfully created <<global cache>>.`, false);
+ this.taskHandler.startAll();
+ }
+
+ public async dbPreInit(): Promise<void> {
+ try {
+ await this.db.authenticate();
+ Models.Global.initModel(this.db);
+ Models.Guild.initModel(this.db, this);
+ Models.ModLog.initModel(this.db);
+ Models.Ban.initModel(this.db);
+ Models.Mute.initModel(this.db);
+ Models.Level.initModel(this.db);
+ Models.StickyRole.initModel(this.db);
+ await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated
+ await this.console.success('Startup', `Successfully connected to <<database>>.`, false);
+ } catch (error) {
+ await this.console.error('Startup', `Failed to connect to <<database>> with error:\n` + error?.stack, false);
+ }
+ }
+
+ /** Starts the bot */
+ public async start(): Promise<void> {
+ try {
+ await this._init();
+ await this.login(this.token);
+ } catch (e) {
+ await this.console.error('Start', chalk.red(e.stack), false);
+ exit(2);
+ }
+ }
+
+ /** Logs out, terminates the connection to Discord, and destroys the client. */
+ public destroy(relogin = false): void | Promise<string> {
+ super.destroy();
+ if (relogin) {
+ 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?.global?.superUsers?.includes(userID) || this.config.owners.includes(userID);
+ }
+}
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
new file mode 100644
index 0000000..94ad10c
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts
@@ -0,0 +1,536 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+import { exec } from 'child_process';
+import { ClientUtil } from 'discord-akairo';
+import { APIMessage } from 'discord-api-types';
+import {
+ ButtonInteraction,
+ ColorResolvable,
+ CommandInteraction,
+ Constants,
+ GuildMember,
+ Message,
+ MessageActionRow,
+ MessageButton,
+ MessageComponentInteraction,
+ MessageEditOptions,
+ MessageEmbed,
+ MessageOptions,
+ MessagePayload,
+ Snowflake,
+ TextChannel,
+ User,
+ Util,
+ WebhookEditMessageOptions
+} from 'discord.js';
+import got from 'got';
+import { promisify } from 'util';
+import { Global } from '../../models';
+import { BushCache } from '../../utils/BushCache';
+import { BushMessage } from '../discord.js/BushMessage';
+import { BushClient } from './BushClient';
+
+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;
+}
+
+interface bushColors {
+ default: '#1FD8F1';
+ error: '#EF4947';
+ warn: '#FEBA12';
+ success: '#3BB681';
+ info: '#3B78FF';
+ red: '#ff0000';
+ blue: '#0055ff';
+ aqua: '#00bbff';
+ purple: '#8400ff';
+ blurple: '#5440cd';
+ pink: '#ff00e6';
+ green: '#00ff1e';
+ darkGreen: '#008f11';
+ gold: '#b59400';
+ yellow: '#ffff00';
+ white: '#ffffff';
+ gray: '#a6a6a6';
+ lightGray: '#cfcfcf';
+ darkGray: '#7a7a7a';
+ black: '#000000';
+ orange: '#E86100';
+}
+export class BushClientUtil extends ClientUtil {
+ /** The client of this ClientUtil */
+ public declare readonly 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}`);
+ }
+ }
+ 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 {
+ return await this.client.users.fetch(text as Snowflake);
+ } catch {
+ // pass
+ }
+ }
+ const mentionReg = /<@!?(?<id>\d{17,19})>/;
+ const mentionMatch = text.match(mentionReg);
+ if (mentionMatch) {
+ try {
+ return await this.client.users.fetch(mentionMatch.groups.id as Snowflake);
+ } 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: bushColors = {
+ default: '#1FD8F1',
+ error: '#EF4947',
+ warn: '#FEBA12',
+ success: '#3BB681',
+ info: '#3B78FF',
+ red: '#ff0000',
+ blue: '#0055ff',
+ aqua: '#00bbff',
+ purple: '#8400ff',
+ blurple: '#5440cd',
+ pink: '#ff00e6',
+ green: '#00ff1e',
+ darkGreen: '#008f11',
+ gold: '#b59400',
+ yellow: '#ffff00',
+ white: '#ffffff',
+ gray: '#a6a6a6',
+ lightGray: '#cfcfcf',
+ darkGray: '#7a7a7a',
+ black: '#000000',
+ orange: '#E86100'
+ };
+
+ /** Commonly Used 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?: ColorResolvable, 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, '');
+ }
+
+ /** 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: Message = 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 || 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(() => undefined);
+ if (msg.deletable && !msg.deleted) {
+ await msg.delete();
+ }
+ } else {
+ await interaction
+ ?.update({ content: `${text ? text + '\n' : ''}Command closed by user.`, embeds: [], components: [] })
+ .catch(() => undefined);
+ }
+ 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(() => undefined);
+ }
+ });
+
+ collector.on('end', async () => {
+ await msg.edit({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow(true)] }).catch(() => undefined);
+ });
+
+ async function edit(interaction: MessageComponentInteraction): Promise<void> {
+ return await interaction
+ ?.update({ content: text, embeds: [embeds[curPage]], components: [getPaginationRow()] })
+ .catch(() => undefined);
+ }
+ 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 });
+ 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(() => undefined);
+ if (msg.deletable && !msg.deleted) {
+ await msg.delete();
+ }
+ return;
+ } else {
+ return await interaction?.deferUpdate().catch(() => undefined);
+ }
+ });
+
+ collector.on('end', async () => {
+ updateOptions(true, true);
+ await msg.edit(options as MessageEditOptions).catch(() => undefined);
+ });
+
+ 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 | MessagePayload | WebhookEditMessageOptions
+ ): Promise<Message | APIMessage> {
+ let newResponseOptions: string | MessagePayload | 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 Message | APIMessage;
+ } else {
+ await interaction.reply(newResponseOptions);
+ return await interaction.fetchReply().catch(() => undefined);
+ }
+ }
+
+ /** Gets the channel configs as a TextChannel */
+ public async getConfigChannel(channel: 'log' | 'error' | 'dm'): Promise<TextChannel> {
+ return (await this.client.channels.fetch(this.client.config.channels[channel])) as TextChannel;
+ }
+
+ /**
+ * Takes an array and combines the elements using the supplied conjunction.
+ *
+ * @param {string[]} array The array to combine.
+ * @param {string} conjunction The conjunction to use.
+ * @param {string} ifEmpty What to return if the array is empty.
+ * @returns The combined elements or `ifEmpty`
+ *
+ * @example
+ * const permissions = oxford(['ADMINISTRATOR', 'SEND_MESSAGES', 'MANAGE_MESSAGES'], 'and', 'none');
+ * console.log(permissions); // ADMINISTRATOR, SEND_MESSAGES and MANAGE_MESSAGES
+ */
+ public oxford(array: string[], conjunction: string, ifEmpty: string): string {
+ const l = array.length;
+ if (!l) return ifEmpty;
+ if (l < 2) return array[0];
+ if (l < 3) return array.join(` ${conjunction} `);
+ array = array.slice();
+ array[l - 1] = `${conjunction} ${array[l - 1]}`;
+ return array.join(', ');
+ }
+
+ public async insertOrRemoveFromGlobal(
+ action: 'add' | 'remove',
+ key: keyof typeof BushCache['global'],
+ value: any
+ ): Promise<Global | void> {
+ const environment = this.client.config.dev ? 'development' : 'production';
+ const row = await Global.findByPk(environment);
+ const oldValue: any[] = row[key];
+ let newValue: any[];
+ if (action === 'add') {
+ if (!oldValue.includes(action)) oldValue.push(value);
+ newValue = oldValue;
+ } else {
+ newValue = oldValue.filter((ae) => ae !== value);
+ }
+ row[key] = newValue;
+ this.client.cache.global[key] = newValue;
+ return await row.save().catch((e) => this.client.logger.error('insertOrRemoveFromGlobal', e));
+ }
+
+ /**
+ * Surrounds a string to the begging an end of each element in an array.
+ *
+ * @param {string[]} array The array you want to surround.
+ * @param {string} surroundChar1 The character placed in the beginning of the element (or end if surroundChar2 isn't supplied).
+ * @param {string} [surroundChar2=surroundChar1] The character placed in the end of the element.
+ * @returns {string[]}
+ */
+ public surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] {
+ const newArray = [];
+ array.forEach((a) => {
+ newArray.push(`${surroundChar1}${a}${surroundChar2 || surroundChar1}`);
+ });
+ return newArray;
+ }
+
+ // public createModLogEntry(
+ // user: User | Snowflake,
+ // guild: Guild | Snowflake,
+ // reason?: string,
+ // type?: ModLogType,
+ // duration?: number,
+ // moderator: User | Snowflake
+ // ): ModLog {
+
+ // }
+}
diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts
new file mode 100644
index 0000000..b7071b5
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushCommand.ts
@@ -0,0 +1,57 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { ArgumentGenerator, ArgumentOptions, ArgumentPromptOptions, Command, CommandOptions } from 'discord-akairo';
+import { Snowflake } from 'discord.js';
+import { BushMessage } from '../discord.js/BushMessage';
+import { BushClient } from './BushClient';
+import { BushCommandHandler } from './BushCommandHandler';
+import { BushSlashMessage } from './BushSlashMessage';
+
+export interface BushArgumentOptions extends ArgumentOptions {
+ id: string;
+ description?: string;
+ prompt?: ArgumentPromptOptions;
+}
+
+export interface BushCommandOptions extends CommandOptions {
+ hidden?: boolean;
+ restrictedChannels?: Snowflake[];
+ restrictedGuilds?: Snowflake[];
+ description: {
+ content: string;
+ usage: string | string[];
+ examples: string | string[];
+ };
+ args?: BushArgumentOptions[] | ArgumentGenerator;
+ category: string;
+}
+
+export class BushCommand extends Command {
+ public declare client: BushClient;
+
+ public declare handler: BushCommandHandler;
+
+ 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;
+ public exec(message: BushMessage | BushSlashMessage, args: any): any {
+ super.exec(message, args);
+ }
+}
diff --git a/src/lib/extensions/discord-akairo/BushCommandHandler.ts b/src/lib/extensions/discord-akairo/BushCommandHandler.ts
new file mode 100644
index 0000000..09baf2e
--- /dev/null
+++ b/src/lib/extensions/discord-akairo/BushCommandHandler.ts
@@ -0,0 +1,90 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { Category, CommandHandler, CommandHandlerOptions } from 'discord-akairo';
+import { Collection } from 'discord.js';
+import { BushConstants } from '../../utils/BushConstants';
+import { BushMessage } from '../discord.js/BushMessage';
+import { BushClient } from './BushClient';
+import { BushCommand } from './BushCommand';
+
+export type BushCommandHandlerOptions = CommandHandlerOptions;
+
+const CommandHandlerEvents = BushConstants.CommandHandlerEvents;
+const BlockedReasons = BushConstants.BlockedReasons;
+
+export class BushCommandHandler extends CommandHandler {
+ public declare client: BushClient;
+ public declare modules: Collection<string, BushCommand>;
+ public declare categories: Collection<string, Category<string, BushCommand>>;
+ public constructor(client: BushClient, options: CommandHandlerOptions) {