diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-10-21 00:05:53 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2021-10-21 00:05:53 -0400 |
commit | 166d7fdf24440db71311c2cda95697c06e7b8b36 (patch) | |
tree | 23b0400362b5f3035b156200eb634d202aa54741 /src/lib | |
parent | 08f33f7d450c8920afc3b9fb8886729547065313 (diff) | |
download | tanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.tar.gz tanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.tar.bz2 tanzanite-166d7fdf24440db71311c2cda95697c06e7b8b36.zip |
Refactoring, rewrote ButtonPaginator, better permission handling + support for send messages in threads, optimizations, another scam link
Diffstat (limited to 'src/lib')
-rw-r--r-- | src/lib/badlinks.ts | 1 | ||||
-rw-r--r-- | src/lib/badwords.ts | 2 | ||||
-rw-r--r-- | src/lib/common/ButtonPaginator.ts | 186 | ||||
-rw-r--r-- | src/lib/common/DeleteButton.ts | 61 | ||||
-rw-r--r-- | src/lib/common/Format.ts | 72 | ||||
-rw-r--r-- | src/lib/common/autoMod.ts | 52 | ||||
-rw-r--r-- | src/lib/common/moderation.ts | 19 | ||||
-rw-r--r-- | src/lib/common/typings/BushInspectOptions.d.ts | 91 | ||||
-rw-r--r-- | src/lib/common/typings/CodeBlockLang.d.ts | 310 | ||||
-rw-r--r-- | src/lib/common/util/Arg.ts | 120 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClientUtil.ts | 856 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushCommand.ts | 4 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuild.ts | 2 | ||||
-rw-r--r-- | src/lib/extensions/discord.js/BushGuildMember.ts | 2 | ||||
-rw-r--r-- | src/lib/models/Guild.ts | 2 | ||||
-rw-r--r-- | src/lib/utils/Config.ts | 72 |
16 files changed, 1031 insertions, 821 deletions
diff --git a/src/lib/badlinks.ts b/src/lib/badlinks.ts index 933fce3..059d3d9 100644 --- a/src/lib/badlinks.ts +++ b/src/lib/badlinks.ts @@ -792,6 +792,7 @@ export default [ "discordcreators.net", "discordd.buzz", "discordd.gg", + "discordd.gift", "discorddaapp.com", "discorddev.com", "discorddiscord.com", diff --git a/src/lib/badwords.ts b/src/lib/badwords.ts index 975df24..e5033d7 100644 --- a/src/lib/badwords.ts +++ b/src/lib/badwords.ts @@ -1,4 +1,4 @@ -import { BadWords, Severity } from "./common/autoMod"; +import { BadWords, Severity } from "./common/AutoMod"; export default { /* -------------------------------------------------------------------------- */ diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts new file mode 100644 index 0000000..c74f6ad --- /dev/null +++ b/src/lib/common/ButtonPaginator.ts @@ -0,0 +1,186 @@ +import { + Constants, + MessageActionRow, + MessageButton, + MessageComponentInteraction, + MessageEmbed, + MessageEmbedOptions +} from 'discord.js'; +import { BushMessage, BushSlashMessage } from '..'; +import { DeleteButton } from './DeleteButton'; + +export class ButtonPaginator { + protected message: BushMessage | BushSlashMessage; + protected embeds: MessageEmbed[] | MessageEmbedOptions[]; + protected text: string | null; + protected deleteOnExit: boolean; + protected curPage: number; + protected sentMessage: BushMessage | undefined; + + /** + * Sends multiple embeds with controls to switch between them + * @param message - The message to respond to + * @param embeds - The embeds to switch between + * @param text - The text send with the embeds (optional) + * @param deleteOnExit - Whether to delete the message when the exit button is clicked (defaults to true) + * @param startOn - The page to start from (**not** the index) + */ + public static async send( + message: BushMessage | BushSlashMessage, + embeds: MessageEmbed[] | MessageEmbedOptions[], + text: string | null = null, + deleteOnExit = true, + startOn = 1 + ): Promise<void> { + // no need to paginate if there is only one page + if (embeds.length === 1) return DeleteButton.send(message, { embeds: embeds }); + + return await new ButtonPaginator(message, embeds, text, deleteOnExit, startOn).send(); + } + + protected get numPages(): number { + return this.embeds.length; + } + + protected constructor( + message: BushMessage | BushSlashMessage, + embeds: MessageEmbed[] | MessageEmbedOptions[], + text: string | null, + deleteOnExit: boolean, + startOn: number + ) { + this.message = message; + this.embeds = embeds; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.text = text || null; + this.deleteOnExit = deleteOnExit; + this.curPage = startOn - 1; + + // add footers + for (let i = 0; i < embeds.length; i++) { + if (embeds[i] instanceof MessageEmbed) { + (embeds[i] as MessageEmbed).setFooter(`Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}`); + } else { + (embeds[i] as MessageEmbedOptions).footer = { + text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` + }; + } + } + } + + protected async send() { + this.sentMessage = (await this.message.util.reply({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow()] + })) as BushMessage; + + const collector = this.sentMessage.createMessageComponentCollector({ + filter: (i) => i.customId.startsWith('paginate_') && i.message.id === this.sentMessage!.id, + time: 300000 + }); + + collector.on('collect', (i) => void this.collect(i)); + collector.on('end', () => void this.end()); + } + + protected async collect(interaction: MessageComponentInteraction) { + if (interaction.user.id !== this.message.author.id || !client.config.owners.includes(interaction.user.id)) + return await interaction?.deferUpdate().catch(() => undefined); + + switch (interaction.customId) { + case 'paginate_beginning': + this.curPage = 0; + return this.edit(interaction); + case 'paginate_back': + this.curPage--; + return await this.edit(interaction); + case 'paginate_stop': + if (this.deleteOnExit) { + await interaction.deferUpdate().catch(() => undefined); + return await this.sentMessage!.delete().catch(() => undefined); + } else { + return await interaction + ?.update({ + content: `${this.text ? `${this.text}\n` : ''}Command closed by user.`, + embeds: [], + components: [] + }) + .catch(() => undefined); + } + case 'paginate_next': + this.curPage++; + return await this.edit(interaction); + case 'paginate_end': + this.curPage = this.embeds.length - 1; + return await this.edit(interaction); + } + } + + protected async end() { + try { + return this.sentMessage!.edit({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow(true)] + }); + } catch (e) { + return undefined; + } + } + + protected async edit(interaction: MessageComponentInteraction) { + try { + return interaction?.update({ + content: this.text, + embeds: [this.embeds[this.curPage]], + components: [this.getPaginationRow()] + }); + } catch (e) { + return undefined; + } + } + + protected getPaginationRow(disableAll = false): MessageActionRow { + return new MessageActionRow().addComponents( + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate_beginning', + emoji: PaginateEmojis.BEGGING, + disabled: disableAll || this.curPage === 0 + }), + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate_back', + emoji: PaginateEmojis.BACK, + disabled: disableAll || this.curPage === 0 + }), + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate_stop', + emoji: PaginateEmojis.STOP, + disabled: disableAll + }), + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate_next', + emoji: PaginateEmojis.FORWARD, + disabled: disableAll || this.curPage === this.numPages - 1 + }), + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate_end', + emoji: PaginateEmojis.END, + disabled: disableAll || this.curPage === this.numPages - 1 + }) + ); + } +} + +export const enum PaginateEmojis { + BEGGING = '853667381335162910', + BACK = '853667410203770881', + STOP = '853667471110570034', + FORWARD = '853667492680564747', + END = '853667514915225640' +} diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts new file mode 100644 index 0000000..7d2e41b --- /dev/null +++ b/src/lib/common/DeleteButton.ts @@ -0,0 +1,61 @@ +import { Constants, MessageActionRow, MessageButton, MessageComponentInteraction, MessageOptions } from 'discord.js'; +import { BushMessage, BushSlashMessage } from '..'; +import { PaginateEmojis } from './ButtonPaginator'; + +export class DeleteButton { + protected messageOptions: MessageOptions; + protected message: BushMessage | BushSlashMessage; + + /** + * Sends a message with a button for the user to delete it. + * @param message - The message to respond to + * @param options - The send message options + */ + static async send(message: BushMessage | BushSlashMessage, options: Omit<MessageOptions, 'components'>) { + return new DeleteButton(message, options).send(); + } + + protected constructor(message: BushMessage | BushSlashMessage, options: MessageOptions) { + this.message = message; + this.messageOptions = options; + } + + protected async send() { + this.updateComponents(); + + const msg = (await this.message.util.reply(this.messageOptions)) as BushMessage; + + const collector = msg.createMessageComponentCollector({ + filter: (interaction) => interaction.customId == 'paginate__stop' && interaction.message.id == msg.id, + time: 300000 + }); + + collector.on('collect', async (interaction: MessageComponentInteraction) => { + await interaction.deferUpdate().catch(() => undefined); + if (interaction.user.id == this.message.author.id || client.config.owners.includes(interaction.user.id)) { + if (msg.deletable && !msg.deleted) await msg.delete(); + } + }); + + collector.on('end', async () => { + this.updateComponents(true, true); + await msg.edit(this.messageOptions).catch(() => undefined); + }); + } + + protected updateComponents(edit = false, disable = false): void { + this.messageOptions.components = [ + new MessageActionRow().addComponents( + new MessageButton({ + style: Constants.MessageButtonStyles.PRIMARY, + customId: 'paginate__stop', + emoji: PaginateEmojis.STOP, + disabled: disable + }) + ) + ]; + if (edit) { + this.messageOptions.reply = undefined; + } + } +} diff --git a/src/lib/common/Format.ts b/src/lib/common/Format.ts new file mode 100644 index 0000000..ba1ee9f --- /dev/null +++ b/src/lib/common/Format.ts @@ -0,0 +1,72 @@ +import { Formatters, Util } from 'discord.js'; +import { CodeBlockLang } from './typings/CodeBlockLang'; + +/** + * Formats and escapes content for formatting + */ +export class Format { + /** + * Wraps the content inside a codeblock with no language. + * @param content The content to wrap. + */ + public static codeBlock(content: string): string; + /** + * Wraps the content inside a codeblock with the specified language. + * @param language The language for the codeblock. + * @param content The content to wrap. + */ + public static codeBlock(language: CodeBlockLang, content: string): string; + public static codeBlock(languageOrContent: string, content?: string): string { + return typeof content === 'undefined' + ? Formatters.codeBlock(Util.escapeCodeBlock(languageOrContent)) + : Formatters.codeBlock(languageOrContent, Util.escapeCodeBlock(languageOrContent)); + } + + /** + * Wraps the content inside \`backticks\`, which formats it as inline code. + * @param content The content to wrap. + */ + public static inlineCode(content: string): string { + return Formatters.inlineCode(Util.escapeInlineCode(content)); + } + + /** + * Formats the content into italic text. + * @param content The content to wrap. + */ + public static italic(content: string): string { + return Formatters.italic(Util.escapeItalic(content)); + } + + /** + * Formats the content into bold text. + * @param content The content to wrap. + */ + public static bold(content: string): string { + return Formatters.bold(Util.escapeBold(content)); + } + + /** + * Formats the content into underscored text. + * @param content The content to wrap. + */ + public static underscore(content: string): string { + return Formatters.underscore(Util.escapeUnderline(content)); + } + + /** + * Formats the content into strike-through text. + * @param content The content to wrap. + */ + public static strikethrough(content: string): string { + return Formatters.strikethrough(Util.escapeStrikethrough(content)); + } + + /** + * Wraps the content inside spoiler (hidden text). + * @param content The content to wrap. + */ + public static spoiler(content: string): string { + return Formatters.spoiler(Util.escapeSpoiler(content)); + } +} diff --git a/src/lib/common/autoMod.ts b/src/lib/common/autoMod.ts index 0bdbebf..312beb3 100644 --- a/src/lib/common/autoMod.ts +++ b/src/lib/common/autoMod.ts @@ -1,17 +1,17 @@ -import { Formatters, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js'; -import badLinksArray from '../../lib/badlinks'; -import badLinksSecretArray from '../../lib/badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array -import badWords from '../../lib/badwords'; +import { GuildMember, MessageActionRow, MessageButton, MessageEmbed, TextChannel } from 'discord.js'; +import badLinksArray from '../badlinks'; +import badLinksSecretArray from '../badlinks-secret'; // I cannot make this public so just make a new file that export defaults an empty array +import badWords from '../badwords'; import { BushButtonInteraction } from '../extensions/discord.js/BushButtonInteraction'; -import { BushGuildMember } from '../extensions/discord.js/BushGuildMember'; import { BushMessage } from '../extensions/discord.js/BushMessage'; -import { Moderation } from './moderation'; +import { Moderation } from './Moderation'; export class AutoMod { private message: BushMessage; public constructor(message: BushMessage) { this.message = message; + if (message.author.id === client.user?.id) return; void this.handle(); } @@ -21,17 +21,10 @@ export class AutoMod { const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? {}; const badLinks: BadWords = {}; - const badLinksSecret: BadWords = {}; - badLinksArray.forEach((link) => { - badLinks[link] = { - severity: Severity.PERM_MUTE, - ignoreSpaces: true, - ignoreCapitalization: true, - reason: 'malicious link' - }; - }); - badLinksSecretArray.forEach((link) => { + const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])]; + + uniqueLinks.forEach((link) => { badLinks[link] = { severity: Severity.PERM_MUTE, ignoreSpaces: true, @@ -43,9 +36,7 @@ export class AutoMod { const result = { ...this.checkWords(customAutomodPhrases), ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? {} : badWords), - ...this.checkWords( - (await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : { ...badLinks, ...badLinksSecret } - ) + ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? {} : badLinks) }; if (Object.keys(result).length === 0) return; @@ -59,9 +50,7 @@ export class AutoMod { embeds: [ { title: 'AutoMod Error', - description: `Unable to find severity information for ${Formatters.inlineCode( - util.discord.escapeInlineCode(highestOffence.word) - )}`, + description: `Unable to find severity information for ${util.format.inlineCode(highestOffence.word)}`, color: util.colors.error } ] @@ -128,7 +117,7 @@ export class AutoMod { break; } default: { - throw new Error('Invalid severity'); + throw new Error(`Invalid severity: ${highestOffence.severity}`); } } @@ -163,8 +152,8 @@ export class AutoMod { .setDescription( `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From**: <#${ this.message.channel.id - }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${util - .surroundArray(Object.keys(offences), '`') + }> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${Object.keys(offences) + .map((key) => `\`${key}\``) .join(', ')}` ) .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`) @@ -194,12 +183,13 @@ export class AutoMod { const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';'); switch (action) { case 'ban': { - const check = await Moderation.permissionCheck( - interaction.member as BushGuildMember, - interaction.guild!.members.cache.get(userId)!, - 'ban', - true - ); + const victim = await interaction.guild!.members.fetch(userId); + const moderator = + interaction.member instanceof GuildMember + ? interaction.member + : await interaction.guild!.members.fetch(interaction.user.id); + + const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true; if (check !== true) return interaction.reply({ diff --git a/src/lib/common/moderation.ts b/src/lib/common/moderation.ts index c8779fc..29d66fa 100644 --- a/src/lib/common/moderation.ts +++ b/src/lib/common/moderation.ts @@ -123,7 +123,7 @@ export class Moderation { const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; const user = (await util.resolveNonCachedUser(options.user))!.id; const guild = client.guilds.resolveId(options.guild)!; - const type = this.#findTypeEnum(options.type)!; + const type = this.findTypeEnum(options.type)!; const entry = ActivePunishment.build( options.extraInfo @@ -144,7 +144,7 @@ export class Moderation { }): Promise<boolean> { const user = await util.resolveNonCachedUser(options.user); const guild = client.guilds.resolveId(options.guild); - const type = this.#findTypeEnum(options.type); + const type = this.findTypeEnum(options.type); if (!user || !guild) return false; @@ -160,18 +160,19 @@ export class Moderation { success = false; }); if (entries) { - // eslint-disable-next-line @typescript-eslint/no-misused-promises - entries.forEach(async (entry) => { - await entry.destroy().catch(async (e) => { + const promises = entries.map(async (entry) => + entry.destroy().catch(async (e) => { await util.handleError('removePunishmentEntry', e); - }); - success = false; - }); + success = false; + }) + ); + + await Promise.all(promises); } return success; } - static #findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { + private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { const typeMap = { ['mute']: ActivePunishmentType.MUTE, ['ban']: ActivePunishmentType.BAN, diff --git a/src/lib/common/typings/BushInspectOptions.d.ts b/src/lib/common/typings/BushInspectOptions.d.ts new file mode 100644 index 0000000..c2a2360 --- /dev/null +++ b/src/lib/common/typings/BushInspectOptions.d.ts @@ -0,0 +1,91 @@ +import { InspectOptions } from 'util'; + +/** + * {@link https://nodejs.org/api/util.html#util_util_inspect_object_options} + */ +export interface BushInspectOptions extends InspectOptions { + /** + * If `true`, object's non-enumerable symbols and properties are included in the + * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries are also included as well as + * user defined prototype properties (excluding method properties). + * + * **Default**: `false`. + */ + showHidden?: boolean | undefined; + /** + * Specifies the number of times to recurse while formatting `object`. This is useful + * for inspecting large objects. To recurse up to the maximum call stack size pass + * `Infinity` or `null`. + * + * **Default**: `2`. + */ + depth?: number | null | undefined; + /** + * If `true`, the output is styled with ANSI color codes. Colors are customizable. See [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors). + * + * **Default**: `false`. + */ + colors?: boolean | undefined; + /** + * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked. + * + * **Default**: `true`. + */ + customInspect?: boolean | undefined; + /** + * If `true`, `Proxy` inspection includes the [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology) objects. + * + * **Default**: `false`. + */ + showProxy?: boolean | undefined; + /** + * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and + * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to include when formatting. Set to `null` or `Infinity` to + * show all elements. Set to `0` or negative to show no elements. + * + * **Default**: `100`. + */ + maxArrayLength?: number | null | undefined; + /** + * Specifies the maximum number of characters to include when formatting. Set to + * `null` or `Infinity` to show all elements. Set to `0` or negative to show no + * characters. + * + * **Default**: `10000`. + */ + maxStringLength?: number | null | undefined; + /** + * The length at which input values are split across multiple lines. Set to + * `Infinity` to format the input as a single line (in combination with compact set + * to `true` or any number >= `1`). + * + * **Default**: `80`. + */ + breakLength?: number | undefined; + /** + * Setting this to `false` causes each object key to be displayed on a new line. It + * will break on new lines in text that is longer than `breakLength`. If set to a + * number, the most `n` inner elements are united on a single line as long as all + * properties fit into `breakLength`. Short array elements are also grouped together. + * + * **Default**: `3` + */ + compact?: boolean | number | undefined; + /** + * If set to `true` or a function, all properties of an object, and `Set` and `Map` + * entries are sorted in the resulting string. If set to `true` the [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used. + * If set to a function, it is used as a [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters). + * + * **Default**: `false`. + */ + sorted?: boolean | ((a: string, b: string) => number) | undefined; + /** + * If set to `true`, getters are inspected. If set to `'get'`, only getters without a + * corresponding setter are inspected. If set to `'set'`, only getters with a + * corresponding setter are inspected. This might cause side effects depending on + * the getter function. + * + * **Default**: `false`. + */ + getters?: 'get' | 'set' | boolean | undefined; +} diff --git a/src/lib/common/typings/CodeBlockLang.d.ts b/src/lib/common/typings/CodeBlockLang.d.ts new file mode 100644 index 0000000..5a1aeba --- /dev/null +++ b/src/lib/common/typings/CodeBlockLang.d.ts @@ -0,0 +1,310 @@ +export type CodeBlockLang = + | '1c' + | 'abnf' + | 'accesslog' + | 'actionscript' + | 'ada' + | 'arduino' + | 'ino' + | 'armasm' + | 'arm' + | 'avrasm' + | 'actionscript' + | 'as' + | 'angelscript' + | 'asc' + | 'apache' + | 'apacheconf' + | 'applescript' + | 'osascript' + | 'arcade' + | 'asciidoc' + | 'adoc' + | 'aspectj' + | 'autohotkey' + | 'autoit' + | 'awk' + | 'mawk' + | 'nawk' + | 'gawk' + | 'bash' + | 'sh' + | 'zsh' + | 'basic' + | 'bnf' + | 'brainfuck' + | 'bf' + | 'csharp' + | 'cs' + | 'c' + | 'h' + | 'cpp' + | 'hpp' + | 'cc' + | 'hh' + | 'c++' + | 'h++' + | 'cxx' + | 'hxx' + | 'cal' + | 'cos' + | 'cls' + | 'cmake' + | 'cmake.in' + | 'coq' + | 'csp' + | 'css' + | 'capnproto' + | 'capnp' + | 'clojure' + | 'clj' + | 'coffeescript' + | 'coffee' + | 'cson' + | 'iced' + | 'crmsh' + | 'crm' + | 'pcmk' + | 'crystal' + | 'cr' + | 'd' + | 'dns' + | 'zone' + | 'bind' + | 'dos' + | 'bat' + | 'cmd' + | 'dart' + | 'dpr' + | 'dfm' + | 'pas' + | 'pascal' + | 'diff' + | 'patch' + | 'django' + | 'jinja' + | 'dockerfile' + | 'docker' + | 'dsconfig' + | 'dts' + | 'dust' + | 'dst' + | 'ebnf' + | 'elixir' + | 'elm' + | 'erlang' + | 'erl' + | 'excel' + | 'xls' + | 'xlsx' + | 'fsharp' + | 'fs' + | 'fix' + | 'fortran' + | 'f90' + | 'f95' + | 'gcode' + | 'nc' + | 'gams' + | 'gms' + | 'gauss' + | 'gss' + | 'gherkin' + | 'go' + | 'golang' + | 'golo' + | 'gololang' + | 'gradle' + | 'groovy' + | 'xml' + | 'html' + | 'xhtml' + | 'rss' + | 'atom' + | 'xjb' + | 'xsd' + | 'xsl' + | 'plist' + | 'svg' + | 'http' + | 'https' + | 'haml' + | 'handlebars' + | 'hbs' + | 'html.hbs' + | 'html.handlebars' + | 'haskell' + | 'hs' + | 'haxe' + | 'hx' + | 'hlsl' + | 'hy' + | 'hylang' + | 'ini' + | 'toml' + | 'inform7' + | 'i7' + | 'irpf90' + | 'json' + | 'java' + | 'jsp' + | 'javascript' + | 'js' + | 'jsx' + | 'julia' + | 'julia-repl' + | 'kotlin' + | 'kt' + | 'tex' + | 'leaf' + | 'lasso' + | 'ls' + | 'lassoscript' + | 'less' + | 'ldif' + | 'lisp' + | 'livecodeserver' + | 'livescript' + | 'ls' + | 'lua' + | 'makefile' + | 'mk' + | 'mak' + | 'make' + | 'markdown' + | 'md' + | 'mkdown' + | 'mkd' + | 'mathematica' + | 'mma' + | 'wl' + | 'matlab' + | 'maxima' + | 'mel' + | 'mercury' + | 'mizar' + | 'mojolicious' + | 'monkey' + | 'moonscript' + | 'moon' + | 'n1ql' + | 'nsis' + | 'nginx' + | 'nginxconf' + | 'nim' + | 'nimrod' + | 'nix' + | 'ocaml' + | 'ml' + | 'objectivec' + | 'mm' + | 'objc' + | 'obj-c' + | 'obj-c++' + | 'objective-c++' + | 'glsl' + | 'openscad' + | 'scad' + | 'ruleslanguage' + | 'oxygene' + | 'pf' + | 'pf.conf' + | 'php' + | 'parser3' + | 'perl' + | 'pl' + | 'pm' + | 'plaintext' + | 'txt' + | 'text' + | 'pony' + | 'pgsql' + | 'postgres' + | 'postgresql' + | 'powershell' + | 'ps' + | 'ps1' + | 'processing' + | 'prolog' + | 'properties' + | 'protobuf' + | 'puppet' + | 'pp' + | 'python' + | 'py' + | 'gyp' + | 'profile' + | 'python-repl' + | 'pycon' + | 'k' + | 'kdb' + | 'qml' + | 'r' + | 'reasonml' + | 're' + | 'rib' + | 'rsl' + | 'graph' + | 'instances' + | 'ruby' + | 'rb' + | 'gemspec' + | 'podspec' + | 'thor' + | 'irb' + | 'rust' + | 'rs' + | 'sas' + | 'scss' + | 'sql' + | 'p21' + | 'step' + | 'stp' + | 'scala' + | 'scheme' + | 'scilab' + | 'sci' + | 'shell' + | 'console' + | 'smali' + | 'smalltalk' + | 'st' + | 'sml' + | 'ml' + | 'stan' + | 'stanfuncs' + | 'stata' + | 'stylus' + | 'styl' + | 'subunit' + | 'swift' + | 'tcl' + | 'tk' + | 'tap' + | 'thrift' + | 'tp' + | 'twig' + | 'craftcms' + | 'typescript' + | 'ts' + | 'vbnet' + | 'vb' + | 'vbscript' + | 'vbs' + | 'vhdl' + | 'vala' + | 'verilog' + | 'v' + | 'vim' + | 'axapta' + | 'x++' + | 'x86asm' + | 'xl' + | 'tao' + | 'xquery' + | 'xpath' + | 'xq' + | 'yml' + | 'yaml' + | 'zephir' + | 'zep'; diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts new file mode 100644 index 0000000..84d5aeb --- /dev/null +++ b/src/lib/common/util/Arg.ts @@ -0,0 +1,120 @@ +import { Argument, ArgumentTypeCaster, Flag, ParsedValuePredicate, TypeResolver } from 'discord-akairo'; +import { Message } from 'discord.js'; +import { BushArgumentType } from '../..'; + +export class Arg { + /** + * Casts a phrase to this argument's type. + * @param type - The type to cast to. + * @param resolver - The type resolver. + * @param message - Message that called the command. + * @param phrase - Phrase to process. + */ + public static cast(type: BushArgumentType, resolver: TypeResolver, message: Message, phrase: string): Promise<any> { + return Argument.cast(type, resolver, message, phrase); + } + + /** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the entire composition fails. + * @param types - Types to use. + */ + public static compose(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.compose(...types); + } + + /** + * Creates a type that is the left-to-right composition of the given types. + * If any of the types fails, the composition still continues with the failure passed on. + * @param types - Types to use. + */ + public static composeWithFailure(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.composeWithFailure(...types); + } + + /** + * Checks if something is null, undefined, or a fail flag. + * @param value - Value to check. + */ + public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) { + return Argument.isFailure(value); + } + + /** + * Creates a type from multiple types (product type). + * Only inputs where each type resolves with a non-void value are valid. + * @param types - Types to use. + */ + public static product(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.product(...types); + } + + /** + * Creates a type where the parsed value must be within a range. + * @param type - The type to use. + * @param min - Minimum value. + * @param max - Maximum value. + * @param inclusive - Whether or not to be inclusive on the upper bound. + */ + public static range(type: BushArgumentType, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster { + return Argument.range(type, min, max, inclusive); + } + + /** + * Creates a type that parses as normal but also tags it with some data. + * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ + public static tagged(type: BushArgumentType, tag?: any): ArgumentTypeCaster { + return Argument.tagged(type, tag); + } + + /** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * Each type will also be tagged using `tagged` with themselves. + * @param types - Types to use. + */ + public static taggedUnion(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.taggedUnion(...types); + } + + /** + * Creates a type that parses as normal but also tags it with some data and carries the original input. + * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. + */ + public static taggedWithInput(type: BushArgumentType, tag?: any): ArgumentTypeCaster { + return Argument.taggedWithInput(type, tag); + } + + /** + * Creates a type from multiple types (union type). + * The first type that resolves to a non-void value is used. + * @param types - Types to use. + */ + public static union(...types: BushArgumentType[]): ArgumentTypeCaster { + return Argument.union(...types); + } + + /** + * Creates a type with extra validation. + * If the predicate is not true, the value is considered invalid. + * @param type - The type to use. + * @param predicate - The predicate function. + */ + public static validate(type: BushArgumentType, predicate: ParsedValuePredicate): ArgumentTypeCaster { + return Argument.validate(type, predicate); + } + + /** + * Creates a type that parses as normal but also carries the original input. + * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. + * @param type - The type to use. + */ + public static withInput(type: BushArgumentType): ArgumentTypeCaster { + return Argument.withInput(type); + } +} diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 448eaf3..499d2c7 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -1,5 +1,4 @@ import { - BushArgumentType, BushCache, BushClient, BushConstants, @@ -11,30 +10,16 @@ import { PronounCode } from '@lib'; import { exec } from 'child_process'; -import { - Argument, - ArgumentTypeCaster, - ClientUtil, - Flag, - ParsedValuePredicate, - TypeResolver, - Util as AkairoUtil -} from 'discord-akairo'; +import { ClientUtil, Util as AkairoUtil } from 'discord-akairo'; import { APIMessage } from 'discord-api-types'; import { ColorResolvable, CommandInteraction, - Constants, GuildMember, InteractionReplyOptions, Message, - MessageActionRow, - MessageButton, - MessageComponentInteraction, - MessageEditOptions, MessageEmbed, - MessageEmbedOptions, - MessageOptions, + PermissionResolvable, Snowflake, TextChannel, ThreadMember, @@ -46,443 +31,16 @@ import got from 'got'; import humanizeDuration from 'humanize-duration'; import _ from 'lodash'; import moment from 'moment'; -import { inspect, InspectOptions, promisify } from 'util'; +import { inspect, promisify } from 'util'; import CommandErrorListener from '../../../listeners/commands/commandError'; +import { Format } from '../../common/Format'; +import { BushInspectOptions } from '../../common/typings/BushInspectOptions'; +import { CodeBlockLang } from '../../common/typings/CodeBlockLang'; +import { Arg } from '../../common/util/Arg'; import { BushNewsChannel } from '../discord.js/BushNewsChannel'; import { BushTextChannel } from '../discord.js/BushTextChannel'; import { BushSlashEditMessageType, BushSlashSendMessageType, BushUserResolvable } 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 MojangProfile { - username: string; - uuid: string; -} - -// #region codeblock type -export type CodeBlockLang = - | '1c' - | 'abnf' - | 'accesslog' - | 'actionscript' - | 'ada' - | 'arduino' - | 'ino' - | 'armasm' - | 'arm' - | 'avrasm' - | 'actionscript' - | 'as' - | 'angelscript' - | 'asc' - | 'apache' - | 'apacheconf' - | 'applescript' - | 'osascript' - | 'arcade' - | 'asciidoc' - | 'adoc' - | 'aspectj' - | 'autohotkey' - | 'autoit' - | 'awk' - | 'mawk' - | 'nawk' - | 'gawk' - | 'bash' - | 'sh' - | 'zsh' - | 'basic' - | 'bnf' - | 'brainfuck' - | 'bf' - | 'csharp' - | 'cs' - | 'c' - | 'h' - | 'cpp' - | 'hpp' - | 'cc' - | 'hh' - | 'c++' - | 'h++' - | 'cxx' - | 'hxx' - | 'cal' - | 'cos' - | 'cls' - | 'cmake' - | 'cmake.in' - | 'coq' - | 'csp' - | 'css' - | 'capnproto' - | 'capnp' - | 'clojure' - | 'clj' - | 'coffeescript' - | 'coffee' - | 'cson' - | 'iced' - | 'crmsh' - | 'crm' - | 'pcmk' - | 'crystal' - | 'cr' - | 'd' - | 'dns' - | 'zone' - | 'bind' - | 'dos' - | 'bat' - | 'cmd' - | 'dart' - | 'dpr' - | 'dfm' - | 'pas' - | 'pascal' - | 'diff' - | 'patch' - | 'django' - | 'jinja' - | 'dockerfile' - | 'docker' - | 'dsconfig' - | 'dts' - | 'dust' - | 'dst' - | 'ebnf' - | 'elixir' - | 'elm' - | 'erlang' - | 'erl' - | 'excel' - | 'xls' - | 'xlsx' - | 'fsharp' - | 'fs' - | 'fix' - | 'fortran' - | 'f90' - | 'f95' - | 'gcode' - | 'nc' - | 'gams' - | 'gms' - | 'gauss' - | 'gss' - | 'gherkin' - | 'go' - | 'golang' - | 'golo' - | 'gololang' - | 'gradle' - | 'groovy' - | 'xml' - | 'html' - | 'xhtml' - | 'rss' - | 'atom' - | 'xjb' - | 'xsd' - | 'xsl' - | 'plist' - | 'svg' - | 'http' - | 'https' - | 'haml' - | 'handlebars' - | 'hbs' - | 'html.hbs' - | 'html.handlebars' - | 'haskell' - | 'hs' - | 'haxe' - | 'hx' - | 'hlsl' - | 'hy' - | 'hylang' - | 'ini' - | 'toml' - | 'inform7' - | 'i7' - | 'irpf90' - | 'json' - | 'java' - | 'jsp' - | 'javascript' - | 'js' - | 'jsx' - | 'julia' - | 'julia-repl' - | 'kotlin' - | 'kt' - | 'tex' - | 'leaf' - | 'lasso' - | 'ls' - | 'lassoscript' - | 'less' - | 'ldif' - | 'lisp' - | 'livecodeserver' - | 'livescript' - | 'ls' - | 'lua' - | 'makefile' - | 'mk' - | 'mak' - | 'make' - | 'markdown' - | 'md' - | 'mkdown' - | 'mkd' - | 'mathematica' - | 'mma' - | 'wl' - | 'matlab' - | 'maxima' - | 'mel' - | 'mercury' - | 'mizar' - | 'mojolicious' - | 'monkey' - | 'moonscript' - | 'moon' - | 'n1ql' - | 'nsis' - | 'nginx' - | 'nginxconf' - | 'nim' - | 'nimrod' - | 'nix' - | 'ocaml' - | 'ml' - | 'objectivec' - | 'mm' - | 'objc' - | 'obj-c' - | 'obj-c++' - | 'objective-c++' - | 'glsl' - | 'openscad' - | 'scad' - | 'ruleslanguage' - | 'oxygene' - | 'pf' - | 'pf.conf' - | 'php' - | 'parser3' - | 'perl' - | 'pl' - | 'pm' - | 'plaintext' - | 'txt' - | 'text' - | 'pony' - | 'pgsql' - | 'postgres' - | 'postgresql' - | 'powershell' - | 'ps' - | 'ps1' - | 'processing' - | 'prolog' - | 'properties' - | 'protobuf' - | 'puppet' - | 'pp' - | 'python' - | 'py' - | 'gyp' - | 'profile' - | 'python-repl' - | 'pycon' - | 'k' - | 'kdb' - | 'qml' - | 'r' - | 'reasonml' - | 're' - | 'rib' - | 'rsl' - | 'graph' - | 'instances' - | 'ruby' - | 'rb' - | 'gemspec' - | 'podspec' - | 'thor' - | 'irb' - | 'rust' - | 'rs' - | 'sas' - | 'scss' - | 'sql' - | 'p21' - | 'step' - | 'stp' - | 'scala' - | 'scheme' - | 'scilab' - | 'sci' - | 'shell' - | 'console' - | 'smali' - | 'smalltalk' - | 'st' - | 'sml' - | 'ml' - | 'stan' - | 'stanfuncs' - | 'stata' - | 'stylus' - | 'styl' - | 'subunit' - | 'swift' - | 'tcl' - | 'tk' - | 'tap' - | 'thrift' - | 'tp' - | 'twig' - | 'craftcms' - | 'typescript' - | 'ts' - | 'vbnet' - | 'vb' - | 'vbscript' - | 'vbs' - | 'vhdl' - | 'vala' - | 'verilog' - | 'v' - | 'vim' - | 'axapta' - | 'x++' - | 'x86asm' - | 'xl' - | 'tao' - | 'xquery' - | 'xpath' - | 'xq' - | 'yml' - | 'yaml' - | 'zephir' - | 'zep'; -//#endregion - -/** - * {@link https://nodejs.org/api/util.html#util_util_inspect_object_options} - */ -export interface BushInspectOptions extends InspectOptions { - /** - * If `true`, object's non-enumerable symbols and properties are included in the - * formatted result. [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) entries are also included as well as - * user defined prototype properties (excluding method properties). - * - * **Default**: `false`. - */ - showHidden?: boolean | undefined; - /** - * Specifies the number of times to recurse while formatting `object`. This is useful - * for inspecting large objects. To recurse up to the maximum call stack size pass - * `Infinity` or `null`. - * - * **Default**: `2`. - */ - depth?: number | null | undefined; - /** - * If `true`, the output is styled with ANSI color codes. Colors are customizable. See [Customizing util.inspect colors](https://nodejs.org/api/util.html#util_customizing_util_inspect_colors). - * - * **Default**: `false`. - */ - colors?: boolean | undefined; - /** - * If `false`, `[util.inspect.custom](depth, opts)` functions are not invoked. - * - * **Default**: `true`. - */ - customInspect?: boolean | undefined; - /** - * If `true`, `Proxy` inspection includes the [`target` and `handler`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology) objects. - * - * **Default**: `false`. - */ - showProxy?: boolean | undefined; - /** - * Specifies the maximum number of `Array`, [`TypedArray`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray), [`WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) and - * [`WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) elements to include when formatting. Set to `null` or `Infinity` to - * show all elements. Set to `0` or negative to show no elements. - * - * **Default**: `100`. - */ - maxArrayLength?: number | null | undefined; - /** - * Specifies the maximum number of characters to include when formatting. Set to - * `null` or `Infinity` to show all elements. Set to `0` or negative to show no - * characters. - * - * **Default**: `10000`. - */ - maxStringLength?: number | null | undefined; - /** - * The length at which input values are split across multiple lines. Set to - * `Infinity` to format the input as a single line (in combination with compact set - * to `true` or any number >= `1`). - * - * **Default**: `80`. - */ - breakLength?: number | undefined; - /** - * Setting this to `false` causes each object key to be displayed on a new line. It - * will break on new lines in text that is longer than `breakLength`. If set to a - * number, the most `n` inner elements are united on a single line as long as all - * properties fit into `breakLength`. Short array elements are also grouped together. - * - * **Default**: `3` - */ - compact?: boolean | number | undefined; - /** - * If set to `true` or a function, all properties of an object, and `Set` and `Map` - * entries are sorted in the resulting string. If set to `true` the [default sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) is used. - * If set to a function, it is used as a [compare function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#parameters). - * - * **Default**: `false`. - */ - sorted?: boolean | ((a: string, b: string) => number) | undefined; - /** - * If set to `true`, getters are inspected. If set to `'get'`, only getters without a - * corresponding setter are inspected. If set to `'set'`, only getters with a - * corresponding setter are inspected. This might cause side effects depending on - * the getter function. - * - * **Default**: `false`. - */ - getters?: 'get' | 'set' | boolean | undefined; -} - export class BushClientUtil extends ClientUtil { /** * The client. @@ -504,22 +62,6 @@ export class BushClientUtil extends ClientUtil { ]; /** - * Emojis used for {@link BushClientUtil.buttonPaginate} - */ - #paginateEmojis = { - beginning: '853667381335162910', - back: '853667410203770881', - stop: '853667471110570034', - forward: '853667492680564747', - end: '853667514915225640' - }; - - /** - * A simple promise exec method - */ - #exec = promisify(exec); - - /** * Creates this client util * @param client The client to initialize with */ @@ -554,7 +96,7 @@ export class BushClientUtil extends ClientUtil { stdout: string; stderr: string; }> { - return await this.#exec(command); + return await promisify(exec)(command); } /** @@ -677,193 +219,13 @@ export class BushClientUtil extends ClientUtil { } /** - * Paginates an array of embeds using buttons. - */ - public async buttonPaginate( - message: BushMessage | BushSlashMessage, - embeds: MessageEmbed[] | MessageEmbedOptions[], - text: string | null = null, - deleteOnExit?: boolean, - startOn?: number - ): Promise<void> { - const paginateEmojis = this.#paginateEmojis; - if (deleteOnExit === undefined) deleteOnExit = true; - - if (embeds.length === 1) { - return this.sendWithDeleteButton(message, { embeds: embeds }); - } - - embeds.forEach((_e, i) => { - embeds[i] instanceof MessageEmbed - ? (embeds[i] as MessageEmbed).setFooter(`Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}`) - : ((embeds[i] as MessageEmbedOptions).footer = { - text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` - }); - }); - - const style = Constants.MessageButtonStyles.PRIMARY; - let curPage = startOn ? startOn - 1 : 0; - if (typeof embeds !== 'object') throw new Error('embeds must be an object'); - const msg = (await message.util.reply({ - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - content: text || null, - embeds: [embeds[curPage]], - components: [getPaginationRow()] - })) as Message; - const filter = (interaction: MessageComponentInteraction) => - interaction.customId.startsWith('paginate_') && interaction.message.id === msg.id; - const collector = msg.createMessageComponentCollector({ filter, time: 300000 }); - collector.on('collect', async (interaction: MessageComponentInteraction) => { - if (interaction.user.id === message.author.id || 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); - } - 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 | BushSlashMessage, options: MessageOptions): Promise<void> { - const paginateEmojis = this.#paginateEmojis; - updateOptions(); - const msg = (await message.util.reply(options as MessageOptions & { split?: false })) as Message; - const filter = (interaction: MessageComponentInteraction) => - interaction.customId == 'paginate__stop' && interaction.message == msg; - const collector = msg.createMessageComponentCollector({ filter, time: 300000 }); - collector.on('collect', async (interaction: MessageComponentInteraction) => { - if (interaction.user.id == message.author.id || 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); - }); - - 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 = 4096 characters * * Embed Field Limit = 1024 characters */ public async codeblock(code: string, length: number, language?: CodeBlockLang, substr = false): Promise<string> { let hasteOut = ''; - code = util.discord.escapeCodeBlock(code); + code = this.discord.escapeCodeBlock(code); const prefix = `\`\`\`${language}\n`; const suffix = '\n```'; language = language ?? 'txt'; @@ -887,7 +249,12 @@ export class BushClientUtil extends ClientUtil { return code3; } - public inspect(code: any, options?: BushInspectOptions): string { + /** + * Uses {@link inspect} with custom defaults + * @param object - The object you would like to inspect + * @param options - The options you would like to use to inspect the object + */ + public inspect(object: any, options?: BushInspectOptions): string { const { showHidden: _showHidden = false, depth: _depth = 2, @@ -914,7 +281,7 @@ export class BushClientUtil extends ClientUtil { sorted: _sorted, getters: _getters }; - return inspect(code, optionsWithDefaults); + return inspect(object, optionsWithDefaults); } #mapCredential(old: string): string { @@ -931,6 +298,7 @@ export class BushClientUtil extends ClientUtil { /** * Redacts credentials from a string + * @param text - The text to redact credentials from */ public redact(text: string) { for (const credentialName in { ...client.config.credentials, dbPassword: client.config.db.password }) { @@ -1028,7 +396,7 @@ export class BushClientUtil extends ClientUtil { const newValue = this.addOrRemoveFromArray(action, oldValue, value); row[key] = newValue; client.cache.global[key] = newValue; - return await row.save().catch((e) => util.handleError('insertOrRemoveFromGlobal', e)); + return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e)); } /** @@ -1233,136 +601,108 @@ export class BushClientUtil extends ClientUtil { return str.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); } - get arg() { - return class Arg { - /** - * Casts a phrase to this argument's type. - * @param type - The type to cast to. - * @param resolver - The type resolver. - * @param message - Message that called the command. - * @param phrase - Phrase to process. - */ - public static cast(type: BushArgumentType, resolver: TypeResolver, message: Message, phrase: string): Promise<any> { - return Argument.cast(type, resolver, message, phrase); - } + public async uploadImageToImgur(image: string) { + const clientId = this.client.config.credentials.imgurClientId; - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the entire composition fails. - * @param types - Types to use. - */ - public static compose(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.compose(...types); - } + const resp = (await got + .post('https://api.imgur.com/3/upload', { + headers: { + // Authorization: `Bearer ${token}`, + Authorization: `Client-ID ${clientId}`, + Accept: 'application/json' + }, + form: { + image: image, + type: 'base64' + }, + followRedirect: true + }) + .json()) as { data: { link: string } }; - /** - * Creates a type that is the left-to-right composition of the given types. - * If any of the types fails, the composition still continues with the failure passed on. - * @param types - Types to use. - */ - public static composeWithFailure(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.composeWithFailure(...types); - } + return resp.data.link; + } - /** - * Checks if something is null, undefined, or a fail flag. - * @param value - Value to check. - */ - public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) { - return Argument.isFailure(value); - } + public userGuildPermCheck(message: BushMessage | BushSlashMessage, permissions: PermissionResolvable) { + const missing = message.member?.permissions.missing(permissions) ?? []; - /** - * Creates a type from multiple types (product type). - * Only inputs where each type resolves with a non-void value are valid. - * @param types - Types to use. - */ - public static product(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.product(...types); - } + return missing.length ? missing : null; + } - /** - * Creates a type where the parsed value must be within a range. - * @param type - The type to use. - * @param min - Minimum value. - * @param max - Maximum value. - * @param inclusive - Whether or not to be inclusive on the upper bound. - */ - public static range(type: BushArgumentType, min: number, max: number, inclusive?: boolean): ArgumentTypeCaster { - return Argument.range(type, min, max, inclusive); - } + public clientGuildPermCheck(message: BushMessage | BushSlashMessage, permissions: PermissionResolvable) { + const missing = message.guild?.me?.permissions.missing(permissions) ?? []; - /** - * Creates a type that parses as normal but also tags it with some data. - * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public static tagged(type: BushArgumentType, tag?: any): ArgumentTypeCaster { - return Argument.tagged(type, tag); - } + return missing.length ? missing : null; + } - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * Each type will also be tagged using `tagged` with themselves. - * @param types - Types to use. - */ - public static taggedUnion(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.taggedUnion(...types); - } + public clientSendAndPermCheck( + message: BushMessage | BushSlashMessage, + permissions: PermissionResolvable = [], + checkChannel = false + ) { + const missing = []; + const sendPerm = message.channel!.isThread() ? 'SEND_MESSAGES' : 'SEND_MESSAGES_IN_THREADS'; - /** - * Creates a type that parses as normal but also tags it with some data and carries the original input. - * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string. - */ - public static taggedWithInput(type: BushArgumentType, tag?: any): ArgumentTypeCaster { - return Argument.taggedWithInput(type, tag); - } + if (!message.guild!.me!.permissionsIn(message.channel!.id!).has(sendPerm)) missing.push(sendPerm); - /** - * Creates a type from multiple types (union type). - * The first type that resolves to a non-void value is used. - * @param types - Types to use. - */ - public static union(...types: BushArgumentType[]): ArgumentTypeCaster { - return Argument.union(...types); - } + missing.push( + ...(checkChannel + ? message.guild!.me!.permissionsIn(message.channel!.id!).missing(permissions) + : this.clientGuildPermCheck(message, permissions) ?? []) + ); - /** - * Creates a type with extra validation. - * If the predicate is not true, the value is considered invalid. - * @param type - The type to use. - * @param predicate - The predicate function. - */ - public static validate(type: BushArgumentType, predicate: ParsedValuePredicate): ArgumentTypeCaster { - return Argument.validate(type, predicate); - } + return missing.length ? missing : null; + } - /** - * Creates a type that parses as normal but also carries the original input. - * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed. - * @param type - The type to use. - */ - public static withInput(type: BushArgumentType): ArgumentTypeCaster { - return Argument.withInput(type); - } - }; + public get arg() { + return Arg; + } + + /** + * Formats and escapes content for formatting + */ + public get format() { + return Format; } /** * Discord.js's Util class */ - get discord() { + public get discord() { return DiscordUtil; } /** * discord-akairo's Util class */ - get akairo() { + public get akairo() { return AkairoUtil; } } + +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 MojangProfile { + username: string; + uuid: string; +} diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 0be128b..22d4aae 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -158,9 +158,9 @@ export interface BushCommandOptions extends Omit<CommandOptions, 'userPermission /** Allow this command to be run in channels that are blacklisted. */ bypassChannelBlacklist?: boolean; /** Permissions required by the client to run this command. */ - clientPermissions?: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; + clientPermissions: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; /** Permissions required by the user to run this command. */ - userPermissions?: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; + userPermissions: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; } export class BushCommand extends Command { diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 8d44cd4..51c2795 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -1,6 +1,6 @@ import { Guild, MessageOptions, UserResolvable } from 'discord.js'; import { RawGuildData } from 'discord.js/typings/rawDataTypes'; -import { Moderation } from '../../common/moderation'; +import { Moderation } from '../../common/Moderation'; import { Guild as GuildDB, GuildFeatures, GuildLogType, GuildModel } from '../../models/Guild'; import { ModLogType } from '../../models/ModLog'; import { BushClient, BushUserResolvable } from '../discord-akairo/BushClient'; diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 954342d..77d03b1 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,6 +1,6 @@ import { GuildMember, MessageEmbed, Partialize, Role } from 'discord.js'; import { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; -import { Moderation } from '../../common/moderation'; +import { Moderation } from '../../common/Moderation'; import { ModLogType } from '../../models/ModLog'; import { BushClient } from '../discord-akairo/BushClient'; import { BushGuild } from './BushGuild'; diff --git a/src/lib/models/Guild.ts b/src/lib/models/Guild.ts index d23bb0f..7b51e61 100644 --- a/src/lib/models/Guild.ts +++ b/src/lib/models/Guild.ts @@ -1,6 +1,6 @@ import { Snowflake } from 'discord.js'; import { DataTypes, Sequelize } from 'sequelize'; -import { BadWords } from '../common/autoMod'; +import { BadWords } from '../common/AutoMod'; import { BushClient } from '../extensions/discord-akairo/BushClient'; import { BaseModel } from './BaseModel'; import { jsonArrayInit, jsonParseGet, jsonParseSet, NEVER_USED } from './__helpers'; diff --git a/src/lib/utils/Config.ts b/src/lib/utils/Config.ts index ed73d40..393dd44 100644 --- a/src/lib/utils/Config.ts +++ b/src/lib/utils/Config.ts @@ -1,25 +1,14 @@ import { Snowflake } from 'discord.js'; -export interface ConfigOptions { - credentials: { token: string; betaToken: string; devToken: string; hypixelApiKey: string; wolframAlphaAppId: string }; - environment: 'production' | 'beta' | 'development'; - owners: Snowflake[]; - prefix: string; - channels: { log: Snowflake; error: Snowflake; dm: Snowflake }; - db: { host: string; port: number; username: string; password: string }; - logging: { db: boolean; verbose: boolean; info: boolean }; - supportGuild: { id: Snowflake; invite: string }; -} - export class Config { - public credentials: { token: string; betaToken: string; devToken: string; hypixelApiKey: string; wolframAlphaAppId: string }; - public environment: 'production' | 'beta' | 'development'; + public credentials: Credentials; + public environment: Environment; public owners: Snowflake[]; public prefix: string; - public channels: { log: Snowflake; error: Snowflake; dm: Snowflake }; - public db: { host: string; port: number; username: string; password: string }; - public logging: { db: boolean; verbose: boolean; info: boolean }; - public supportGuild: { id: Snowflake; invite: string }; + public channels: Channels; + public db: DataBase; + public logging: Logging; + public supportGuild: SupportGuild; public constructor(options: ConfigOptions) { this.credentials = options.credentials; @@ -43,10 +32,59 @@ export class Config { public get isProduction(): boolean { return this.environment === 'production'; } + public get isBeta(): boolean { return this.environment === 'beta'; } + public get isDevelopment(): boolean { return this.environment === 'development'; } } + +export interface ConfigOptions { + credentials: Credentials; + environment: Environment; + owners: Snowflake[]; + prefix: string; + channels: Channels; + db: DataBase; + logging: Logging; + supportGuild: SupportGuild; +} + +interface Credentials { + token: string; + betaToken: string; + devToken: string; + hypixelApiKey: string; + wolframAlphaAppId: string; + imgurClientId: string; + imgurClientSecret: string; +} + +type Environment = 'production' | 'beta' | 'development'; + +interface Channels { + log: Snowflake; + error: Snowflake; + dm: Snowflake; +} + +interface DataBase { + host: string; + port: number; + username: string; + password: string; +} + +interface Logging { + db: boolean; + verbose: boolean; + info: boolean; +} + +interface SupportGuild { + id: Snowflake; + invite: string; +} |