diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-08-18 22:42:12 -0400 |
commit | 2356d2c44736fb83021dacb551625852111c8ce6 (patch) | |
tree | 10408d22fdd7a358d2f5c5917c3b59e55aa4c19d /src/lib/common | |
parent | 8aed6f93f7740c592cbc0e2f9fd3269c05286077 (diff) | |
download | tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.gz tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.tar.bz2 tanzanite-2356d2c44736fb83021dacb551625852111c8ce6.zip |
restructure, experimental presence and member automod, fixed bugs probably made some more bugs
Diffstat (limited to 'src/lib/common')
-rw-r--r-- | src/lib/common/AutoMod.ts | 529 | ||||
-rw-r--r-- | src/lib/common/ButtonPaginator.ts | 219 | ||||
-rw-r--r-- | src/lib/common/ConfirmationPrompt.ts | 64 | ||||
-rw-r--r-- | src/lib/common/DeleteButton.ts | 78 | ||||
-rw-r--r-- | src/lib/common/HighlightManager.ts | 485 | ||||
-rw-r--r-- | src/lib/common/Sentry.ts | 24 | ||||
-rw-r--r-- | src/lib/common/tags.ts | 34 | ||||
-rw-r--r-- | src/lib/common/typings/BushInspectOptions.ts | 123 | ||||
-rw-r--r-- | src/lib/common/typings/CodeBlockLang.ts | 311 | ||||
-rw-r--r-- | src/lib/common/util/Arg.ts | 192 | ||||
-rw-r--r-- | src/lib/common/util/Format.ts | 119 | ||||
-rw-r--r-- | src/lib/common/util/Minecraft.ts | 349 | ||||
-rw-r--r-- | src/lib/common/util/Minecraft_Test.ts | 86 | ||||
-rw-r--r-- | src/lib/common/util/Moderation.ts | 556 |
14 files changed, 0 insertions, 3169 deletions
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts deleted file mode 100644 index 44c6dee..0000000 --- a/src/lib/common/AutoMod.ts +++ /dev/null @@ -1,529 +0,0 @@ -import { colors, emojis, format, formatError, Moderation, unmuteResponse } from '#lib'; -import assert from 'assert/strict'; -import chalk from 'chalk'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, - GuildMember, - PermissionFlagsBits, - type ButtonInteraction, - type Message, - type Snowflake, - type TextChannel -} from 'discord.js'; -import UnmuteCommand from '../../commands/moderation/unmute.js'; - -/** - * Handles auto moderation functionality. - */ -export class AutoMod { - /** - * Whether or not a punishment has already been given to the user - */ - private punished = false; - - /** - * @param message The message to check and potentially perform automod actions to - */ - public constructor(private message: Message) { - if (message.author.id === message.client.user?.id) return; - void this.handle(); - } - - /** - * Whether or not the message author is immune to auto moderation - */ - private get isImmune() { - if (!this.message.inGuild()) return false; - assert(this.message.member); - - if (this.message.author.isOwner()) return true; - if (this.message.guild.ownerId === this.message.author.id) return true; - if (this.message.member.permissions.has('Administrator')) return true; - - return false; - } - - /** - * Handles the auto moderation - */ - private async handle() { - if (!this.message.inGuild()) return; - if (!(await this.message.guild.hasFeature('automod'))) return; - if (this.message.author.bot) return; - - traditional: { - if (this.isImmune) break traditional; - const badLinksArray = this.message.client.utils.getShared('badLinks'); - const badLinksSecretArray = this.message.client.utils.getShared('badLinksSecret'); - const badWordsRaw = this.message.client.utils.getShared('badWords'); - - const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? []; - const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])]; - - const badLinks: BadWordDetails[] = uniqueLinks.map((link) => ({ - match: link, - severity: Severity.PERM_MUTE, - ignoreSpaces: false, - ignoreCapitalization: true, - reason: 'malicious link', - regex: false - })); - - const parsedBadWords = Object.values(badWordsRaw).flat(); - - const result = [ - ...this.checkWords(customAutomodPhrases), - ...this.checkWords((await this.message.guild.hasFeature('excludeDefaultAutomod')) ? [] : parsedBadWords), - ...this.checkWords((await this.message.guild.hasFeature('excludeAutomodScamLinks')) ? [] : badLinks) - ]; - - if (result.length === 0) break traditional; - - const highestOffence = result.sort((a, b) => b.severity - a.severity)[0]; - - if (highestOffence.severity === undefined || highestOffence.severity === null) { - void this.message.guild.sendLogChannel('error', { - embeds: [ - { - title: 'AutoMod Error', - description: `Unable to find severity information for ${format.inlineCode(highestOffence.match)}`, - color: colors.error - } - ] - }); - } else { - const color = this.punish(highestOffence); - void this.log(highestOffence, color, result); - } - } - - other: { - if (this.isImmune) break other; - if (!this.punished && (await this.message.guild.hasFeature('delScamMentions'))) void this.checkScamMentions(); - } - - if (!this.punished && (await this.message.guild.hasFeature('perspectiveApi'))) void this.checkPerspectiveApi(); - } - - /** - * Checks if any of the words provided are in the message - * @param words The words to check for - * @returns The blacklisted words found in the message - */ - private checkWords(words: BadWordDetails[]): BadWordDetails[] { - if (words.length === 0) return []; - - const matchedWords: BadWordDetails[] = []; - for (const word of words) { - if (word.regex) { - if (new RegExp(word.match).test(this.format(word.match, word))) { - matchedWords.push(word); - } - } else { - if (this.format(this.message.content, word).includes(this.format(word.match, word))) { - matchedWords.push(word); - } - } - } - return matchedWords; - } - - /** - * If the message contains '@everyone' or '@here' and it contains a common scam phrase, it will be deleted - * @returns - */ - private async checkScamMentions() { - const includes = (c: string) => this.message.content.toLocaleLowerCase().includes(c); - if (!includes('@everyone') && !includes('@here')) return; - // It would be bad if we deleted a message that actually pinged @everyone or @here - if ( - this.message.member?.permissionsIn(this.message.channelId).has(PermissionFlagsBits.MentionEveryone) || - this.message.mentions.everyone - ) - return; - - if ( - includes('steam') || - includes('www.youtube.com') || - includes('youtu.be') || - includes('nitro') || - includes('1 month') || - includes('3 months') || - includes('personalize your profile') || - includes('even more') || - includes('xbox and discord') || - includes('left over') || - includes('check this lol') || - includes('airdrop') - ) { - const color = this.punish({ severity: Severity.TEMP_MUTE, reason: 'everyone mention and scam phrase' } as BadWordDetails); - void this.message.guild!.sendLogChannel('automod', { - embeds: [ - new EmbedBuilder() - .setTitle(`[Severity ${Severity.TEMP_MUTE}] Mention Scam Deleted`) - .setDescription( - `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})` - ) - .addFields({ - name: 'Message Content', - value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` - }) - .setColor(color) - .setTimestamp() - ], - components: [this.buttons(this.message.author.id, 'everyone mention and scam phrase')] - }); - } - } - - private async checkPerspectiveApi() { - return; - if (!this.message.client.config.isDevelopment) return; - - if (!this.message.content) return; - this.message.client.perspective.comments.analyze( - { - key: this.message.client.config.credentials.perspectiveApiKey, - resource: { - comment: { - text: this.message.content - }, - requestedAttributes: { - TOXICITY: {}, - SEVERE_TOXICITY: {}, - IDENTITY_ATTACK: {}, - INSULT: {}, - PROFANITY: {}, - THREAT: {}, - SEXUALLY_EXPLICIT: {}, - FLIRTATION: {} - } - } - }, - (err: any, response: any) => { - if (err) return console.log(err?.message); - - const normalize = (val: number, min: number, max: number) => (val - min) / (max - min); - - const color = (val: number) => { - if (val >= 0.5) { - const x = 194 - Math.round(normalize(val, 0.5, 1) * 194); - return chalk.rgb(194, x, 0)(val); - } else { - const x = Math.round(normalize(val, 0, 0.5) * 194); - return chalk.rgb(x, 194, 0)(val); - } - }; - - console.log(chalk.cyan(this.message.content)); - Object.entries(response.data.attributeScores) - .sort(([a], [b]) => a.localeCompare(b)) - .forEach(([key, value]: any[]) => console.log(chalk.white(key), color(value.summaryScore.value))); - } - ); - } - - /** - * Format a string according to the word options - * @param string The string to format - * @param wordOptions The word options to format with - * @returns The formatted string - */ - private format(string: string, wordOptions: BadWordDetails) { - const temp = wordOptions.ignoreCapitalization ? string.toLowerCase() : string; - return wordOptions.ignoreSpaces ? temp.replace(/ /g, '') : temp; - } - - /** - * Punishes the user based on the severity of the offense - * @param highestOffence The highest offense to punish the user for - * @returns The color of the embed that the log should, based on the severity of the offense - */ - private punish(highestOffence: BadWordDetails) { - let color; - switch (highestOffence.severity) { - case Severity.DELETE: { - color = colors.lightGray; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - this.punished = true; - break; - } - case Severity.WARN: { - color = colors.yellow; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushWarn({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}` - }); - this.punished = true; - break; - } - case Severity.TEMP_MUTE: { - color = colors.orange; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushMute({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}`, - duration: 900_000 // 15 minutes - }); - this.punished = true; - break; - } - case Severity.PERM_MUTE: { - color = colors.red; - void this.message.delete().catch((e) => deleteError.bind(this, e)); - void this.message.member?.bushMute({ - moderator: this.message.guild!.members.me!, - reason: `[AutoMod] ${highestOffence.reason}`, - duration: 0 // permanent - }); - this.punished = true; - break; - } - default: { - throw new Error(`Invalid severity: ${highestOffence.severity}`); - } - } - - return color; - - async function deleteError(this: AutoMod, e: Error | any) { - void this.message.guild?.sendLogChannel('error', { - embeds: [ - { - title: 'AutoMod Error', - description: `Unable to delete triggered message.`, - fields: [{ name: 'Error', value: await this.message.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }], - color: colors.error - } - ] - }); - } - } - - /** - * Log an automod infraction to the guild's specified automod log channel - * @param highestOffence The highest severity word found in the message - * @param color The color that the log embed should be (based on the severity) - * @param offenses The other offenses that were also matched in the message - */ - private async log(highestOffence: BadWordDetails, color: number, offenses: BadWordDetails[]) { - void this.message.client.console.info( - 'autoMod', - `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${ - this.message.author.id - }>>) in <<#${(this.message.channel as TextChannel).name}>> in <<${this.message.guild!.name}>>` - ); - - await this.message.guild!.sendLogChannel('automod', { - embeds: [ - new EmbedBuilder() - .setTitle(`[Severity ${highestOffence.severity}] Automod Action Performed`) - .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:** ${offenses.map((o) => `\`${o.match}\``).join(', ')}` - ) - .addFields({ - name: 'Message Content', - value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` - }) - .setColor(color) - .setTimestamp() - .setAuthor({ name: this.message.author.tag, url: this.message.author.displayAvatarURL() }) - ], - components: highestOffence.severity >= 2 ? [this.buttons(this.message.author.id, highestOffence.reason)] : undefined - }); - } - - private buttons(userId: Snowflake, reason: string): ActionRowBuilder<ButtonBuilder> { - return new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Danger, - label: 'Ban User', - customId: `automod;ban;${userId};${reason}` - }), - new ButtonBuilder({ - style: ButtonStyle.Success, - label: 'Unmute User', - customId: `automod;unmute;${userId}` - }) - ); - } - - /** - * Handles the ban button in the automod log. - * @param interaction The button interaction. - */ - public static async handleInteraction(interaction: ButtonInteraction) { - if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers)) - return interaction.reply({ - content: `${emojis.error} You are missing the **Ban Members** permission.`, - ephemeral: true - }); - const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as [ - 'ban' | 'unmute', - string, - string - ]; - - if (!(['ban', 'unmute'] as const).includes(action)) throw new TypeError(`Invalid automod button action: ${action}`); - - const victim = await interaction.guild!.members.fetch(userId).catch(() => null); - const moderator = - interaction.member instanceof GuildMember - ? interaction.member - : await interaction.guild!.members.fetch(interaction.user.id); - - switch (action) { - case 'ban': { - if (!interaction.guild?.members.me?.permissions.has('BanMembers')) - return interaction.reply({ - content: `${emojis.error} I do not have permission to ${action} members.`, - ephemeral: true - }); - - const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true; - if (check !== true) return interaction.reply({ content: check, ephemeral: true }); - - const result = await interaction.guild?.bushBan({ - user: userId, - reason, - moderator: interaction.user.id, - evidence: (interaction.message as Message).url ?? undefined - }); - - const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId; - - const content = (() => { - if (result === unmuteResponse.SUCCESS) { - return `${emojis.success} Successfully banned ${format.input(victimUserFormatted)}.`; - } else if (result === unmuteResponse.DM_ERROR) { - return `${emojis.warn} Banned ${format.input(victimUserFormatted)} however I could not send them a dm.`; - } else { - return `${emojis.error} Could not ban ${format.input(victimUserFormatted)}: \`${result}\` .`; - } - })(); - - return interaction.reply({ - content: content, - ephemeral: true - }); - } - - case 'unmute': { - if (!victim) - return interaction.reply({ - content: `${emojis.error} Cannot find member, they may have left the server.`, - ephemeral: true - }); - - if (!interaction.guild) - return interaction.reply({ - content: `${emojis.error} This is weird, I don't seem to be in the server...`, - ephemeral: true - }); - - const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true); - if (check !== true) return interaction.reply({ content: check, ephemeral: true }); - - const check2 = await Moderation.checkMutePermissions(interaction.guild); - if (check2 !== true) - return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true }); - - const result = await victim.bushUnmute({ - reason, - moderator: interaction.member as GuildMember, - evidence: (interaction.message as Message).url ?? undefined - }); - - const victimUserFormatted = victim.user.tag; - - const content = (() => { - if (result === unmuteResponse.SUCCESS) { - return `${emojis.success} Successfully unmuted ${format.input(victimUserFormatted)}.`; - } else if (result === unmuteResponse.DM_ERROR) { - return `${emojis.warn} Unmuted ${format.input(victimUserFormatted)} however I could not send them a dm.`; - } else { - return `${emojis.error} Could not unmute ${format.input(victimUserFormatted)}: \`${result}\` .`; - } - })(); - - return interaction.reply({ - content: content, - ephemeral: true - }); - } - } - } -} - -/** - * The severity of the blacklisted word - */ -export const enum Severity { - /** - * Delete message - */ - DELETE, - - /** - * Delete message and warn user - */ - WARN, - - /** - * Delete message and mute user for 15 minutes - */ - TEMP_MUTE, - - /** - * Delete message and mute user permanently - */ - PERM_MUTE -} - -/** - * Details about a blacklisted word - */ -export interface BadWordDetails { - /** - * The word that is blacklisted - */ - match: string; - - /** - * The severity of the word - */ - severity: Severity | 1 | 2 | 3; - - /** - * Whether or not to ignore spaces when checking for the word - */ - ignoreSpaces: boolean; - - /** - * Whether or not to ignore case when checking for the word - */ - ignoreCapitalization: boolean; - - /** - * The reason that this word is blacklisted (used for the punishment reason) - */ - reason: string; - - /** - * Whether or not the word is regex - */ - regex: boolean; -} - -/** - * Blacklisted words mapped to their details - */ -export interface BadWords { - [category: string]: BadWordDetails[]; -} diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts deleted file mode 100644 index 02c78ea..0000000 --- a/src/lib/common/ButtonPaginator.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { DeleteButton, type CommandMessage, type SlashMessage } from '#lib'; -import { CommandUtil } from 'discord-akairo'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - EmbedBuilder, - type APIEmbed, - type Message, - type MessageComponentInteraction -} from 'discord.js'; - -/** - * Sends multiple embeds with controls to switch between them - */ -export class ButtonPaginator { - /** - * The current page of the paginator - */ - protected curPage: number; - - /** - * The paginator message - */ - protected sentMessage: Message | undefined; - - /** - * @param message The message that triggered the command - * @param embeds The embeds to switch between - * @param text The optional text to send with the paginator - * @param {} [deleteOnExit=true] Whether the paginator message gets deleted when the exit button is pressed - * @param startOn The page to start from (**not** the index) - */ - protected constructor( - protected message: CommandMessage | SlashMessage, - protected embeds: EmbedBuilder[] | APIEmbed[], - protected text: string | null, - protected deleteOnExit: boolean, - startOn: number - ) { - this.curPage = startOn - 1; - - // add footers - for (let i = 0; i < embeds.length; i++) { - if (embeds[i] instanceof EmbedBuilder) { - (embeds[i] as EmbedBuilder).setFooter({ text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` }); - } else { - (embeds[i] as APIEmbed).footer = { - text: `Page ${(i + 1).toLocaleString()}/${embeds.length.toLocaleString()}` - }; - } - } - } - - /** - * The number of pages in the paginator - */ - protected get numPages(): number { - return this.embeds.length; - } - - /** - * Sends the paginator message - */ - protected async send() { - this.sentMessage = await this.message.util.reply({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow()] - }); - - const collector = this.sentMessage.createMessageComponentCollector({ - filter: (i) => i.customId.startsWith('paginate_'), - time: 300_000 - }); - collector.on('collect', (i) => void this.collect(i)); - collector.on('end', () => void this.end()); - } - - /** - * Handles interactions with the paginator - * @param interaction The interaction received - */ - protected async collect(interaction: MessageComponentInteraction) { - if (interaction.user.id !== this.message.author.id && !this.message.client.config.owners.includes(interaction.user.id)) - return await interaction?.deferUpdate().catch(() => null); - - switch (interaction.customId) { - case 'paginate_beginning': - this.curPage = 0; - await this.edit(interaction); - break; - case 'paginate_back': - this.curPage--; - await this.edit(interaction); - break; - case 'paginate_stop': - if (this.deleteOnExit) { - await interaction.deferUpdate().catch(() => null); - await this.sentMessage!.delete().catch(() => null); - break; - } else { - await interaction - ?.update({ - content: `${this.text ? `${this.text}\n` : ''}Command closed by user.`, - embeds: [], - components: [] - }) - .catch(() => null); - break; - } - case 'paginate_next': - this.curPage++; - await this.edit(interaction); - break; - case 'paginate_end': - this.curPage = this.embeds.length - 1; - await this.edit(interaction); - break; - } - } - - /** - * Ends the paginator - */ - protected async end() { - if (this.sentMessage && !CommandUtil.deletedMessages.has(this.sentMessage.id)) - await this.sentMessage - .edit({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow(true)] - }) - .catch(() => null); - } - - /** - * Edits the paginator message - * @param interaction The interaction received - */ - protected async edit(interaction: MessageComponentInteraction) { - await interaction - ?.update({ - content: this.text, - embeds: [this.embeds[this.curPage]], - components: [this.getPaginationRow()] - }) - .catch(() => null); - } - - /** - * Generates the pagination row based on the class properties - * @param disableAll Whether to disable all buttons - * @returns The generated {@link ActionRow} - */ - protected getPaginationRow(disableAll = false) { - return new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_beginning', - emoji: PaginateEmojis.BEGINNING, - disabled: disableAll || this.curPage === 0 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_back', - emoji: PaginateEmojis.BACK, - disabled: disableAll || this.curPage === 0 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_stop', - emoji: PaginateEmojis.STOP, - disabled: disableAll - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_next', - emoji: PaginateEmojis.FORWARD, - disabled: disableAll || this.curPage === this.numPages - 1 - }), - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate_end', - emoji: PaginateEmojis.END, - disabled: disableAll || this.curPage === this.numPages - 1 - }) - ); - } - - /** - * 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: CommandMessage | SlashMessage, - embeds: EmbedBuilder[] | APIEmbed[], - text: string | null = null, - deleteOnExit = true, - startOn = 1 - ) { - // 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(); - } -} - -export const PaginateEmojis = { - BEGINNING: { id: '853667381335162910', name: 'w_paginate_beginning', animated: false } as const, - BACK: { id: '853667410203770881', name: 'w_paginate_back', animated: false } as const, - STOP: { id: '853667471110570034', name: 'w_paginate_stop', animated: false } as const, - FORWARD: { id: '853667492680564747', name: 'w_paginate_next', animated: false } as const, - END: { id: '853667514915225640', name: 'w_paginate_end', animated: false } as const -} as const; diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts deleted file mode 100644 index b87d9ef..0000000 --- a/src/lib/common/ConfirmationPrompt.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { type CommandMessage, type SlashMessage } from '#lib'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type MessageComponentInteraction, type MessageOptions } from 'discord.js'; - -/** - * Sends a message with buttons for the user to confirm or cancel the action. - */ -export class ConfirmationPrompt { - /** - * @param message The message that triggered the command - * @param messageOptions Options for sending the message - */ - protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} - - /** - * Sends a message with buttons for the user to confirm or cancel the action. - */ - protected async send(): Promise<boolean> { - this.messageOptions.components = [ - new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder({ style: ButtonStyle.Success, customId: 'confirmationPrompt_confirm', label: 'Yes' }), - new ButtonBuilder({ style: ButtonStyle.Danger, customId: 'confirmationPrompt_cancel', label: 'No' }) - ) - ]; - - const msg = await this.message.channel!.send(this.messageOptions); - - return await new Promise<boolean>((resolve) => { - let responded = false; - const collector = msg.createMessageComponentCollector({ - filter: (interaction) => interaction.message?.id == msg.id, - time: 300_000 - }); - - collector.on('collect', async (interaction: MessageComponentInteraction) => { - await interaction.deferUpdate().catch(() => undefined); - if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { - if (interaction.customId === 'confirmationPrompt_confirm') { - responded = true; - collector.stop(); - resolve(true); - } else if (interaction.customId === 'confirmationPrompt_cancel') { - responded = true; - collector.stop(); - resolve(false); - } - } - }); - - collector.on('end', async () => { - await msg.delete().catch(() => undefined); - if (!responded) resolve(false); - }); - }); - } - - /** - * Sends a message with buttons for the user to confirm or cancel the action. - * @param message The message that triggered the command - * @param sendOptions Options for sending the message - */ - public static async send(message: CommandMessage | SlashMessage, sendOptions: MessageOptions): Promise<boolean> { - return new ConfirmationPrompt(message, sendOptions).send(); - } -} diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts deleted file mode 100644 index 340d07f..0000000 --- a/src/lib/common/DeleteButton.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { PaginateEmojis, type CommandMessage, type SlashMessage } from '#lib'; -import { CommandUtil } from 'discord-akairo'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - MessageComponentInteraction, - MessageEditOptions, - MessagePayload, - type MessageOptions -} from 'discord.js'; - -/** - * Sends a message with a button for the user to delete it. - */ -export class DeleteButton { - /** - * @param message The message to respond to - * @param messageOptions The send message options - */ - protected constructor(protected message: CommandMessage | SlashMessage, protected messageOptions: MessageOptions) {} - - /** - * Sends a message with a button for the user to delete it. - */ - protected async send() { - this.updateComponents(); - - const msg = await this.message.util.reply(this.messageOptions); - - 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 || this.message.client.config.owners.includes(interaction.user.id)) { - if (msg.deletable && !CommandUtil.deletedMessages.has(msg.id)) await msg.delete(); - } - }); - - collector.on('end', async () => { - this.updateComponents(true, true); - await msg.edit(<string | MessagePayload | MessageEditOptions>this.messageOptions).catch(() => undefined); - }); - } - - /** - * Generates the components for the message - * @param edit Whether or not the message is being edited - * @param disable Whether or not to disable the buttons - */ - protected updateComponents(edit = false, disable = false): void { - this.messageOptions.components = [ - new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder({ - style: ButtonStyle.Primary, - customId: 'paginate__stop', - emoji: PaginateEmojis.STOP, - disabled: disable - }) - ) - ]; - if (edit) { - this.messageOptions.reply = undefined; - } - } - - /** - * 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 - */ - public static async send(message: CommandMessage | SlashMessage, options: Omit<MessageOptions, 'components'>) { - return new DeleteButton(message, options).send(); - } -} diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts deleted file mode 100644 index 4f891b7..0000000 --- a/src/lib/common/HighlightManager.ts +++ /dev/null @@ -1,485 +0,0 @@ -import { addToArray, format, Highlight, removeFromArray, timestamp, type HighlightWord } from '#lib'; -import assert from 'assert/strict'; -import { - ChannelType, - Collection, - GuildMember, - type Channel, - type Client, - type Message, - type Snowflake, - type TextBasedChannel -} from 'discord.js'; -import { colors, Time } from '../utils/BushConstants.js'; -import { sanitizeInputForDiscord } from './util/Format.js'; - -const NOTIFY_COOLDOWN = 5 * Time.Minute; -const OWNER_NOTIFY_COOLDOWN = 5 * Time.Minute; -const LAST_MESSAGE_COOLDOWN = 5 * Time.Minute; - -type users = Set<Snowflake>; -type channels = Set<Snowflake>; -type word = HighlightWord; -type guild = Snowflake; -type user = Snowflake; -type lastMessage = Date; -type lastDM = Message; - -type lastDmInfo = [lastDM: lastDM, guild: guild, channel: Snowflake, highlights: HighlightWord[]]; - -export class HighlightManager { - /** - * Cached guild highlights. - */ - public readonly guildHighlights = new Collection<guild, Collection<word, users>>(); - - //~ /** - //~ * Cached global highlights. - //~ */ - //~ public readonly globalHighlights = new Collection<word, users>(); - - /** - * A collection of cooldowns of when a user last sent a message in a particular guild. - */ - public readonly userLastTalkedCooldown = new Collection<guild, Collection<user, lastMessage>>(); - - /** - * Users that users have blocked - */ - public readonly userBlocks = new Collection<guild, Collection<user, users>>(); - - /** - * Channels that users have blocked - */ - public readonly channelBlocks = new Collection<guild, Collection<user, channels>>(); - - /** - * A collection of cooldowns of when the bot last sent each user a highlight message. - */ - public readonly lastedDMedUserCooldown = new Collection<user, lastDmInfo>(); - - /** - * @param client The client to use. - */ - public constructor(public readonly client: Client) {} - - /** - * Sync the cache with the database. - */ - public async syncCache(): Promise<void> { - const highlights = await Highlight.findAll(); - - this.guildHighlights.clear(); - - for (const highlight of highlights) { - highlight.words.forEach((word) => { - if (!this.guildHighlights.has(highlight.guild)) this.guildHighlights.set(highlight.guild, new Collection()); - const guildCache = this.guildHighlights.get(highlight.guild)!; - if (!guildCache.get(word)) guildCache.set(word, new Set()); - guildCache.get(word)!.add(highlight.user); - }); - - if (!this.userBlocks.has(highlight.guild)) this.userBlocks.set(highlight.guild, new Collection()); - this.userBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedUsers)); - - if (!this.channelBlocks.has(highlight.guild)) this.channelBlocks.set(highlight.guild, new Collection()); - this.channelBlocks.get(highlight.guild)!.set(highlight.user, new Set(highlight.blacklistedChannels)); - } - } - - /** - * Checks a message for highlights. - * @param message The message to check. - * @returns A collection users mapped to the highlight matched - */ - public checkMessage(message: Message): Collection<Snowflake, HighlightWord> { - // even if there are multiple matches, only the first one is returned - const ret = new Collection<Snowflake, HighlightWord>(); - if (!message.content || !message.inGuild()) return ret; - if (!this.guildHighlights.has(message.guildId)) return ret; - - const guildCache = this.guildHighlights.get(message.guildId)!; - - for (const [word, users] of guildCache.entries()) { - if (!this.isMatch(message.content, word)) continue; - - for (const user of users) { - if (ret.has(user)) continue; - - if (!message.channel.permissionsFor(user)?.has('ViewChannel')) continue; - - const blockedUsers = this.userBlocks.get(message.guildId)?.get(user) ?? new Set(); - if (blockedUsers.has(message.author.id)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the user <<${ - message.author.tag - }>>` - ); - continue; - } - const blockedChannels = this.channelBlocks.get(message.guildId)?.get(user) ?? new Set(); - if (blockedChannels.has(message.channel.id)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> blocked the channel <<${ - message.channel.name - }>>` - ); - continue; - } - if (message.mentions.has(user)) { - void this.client.console.verbose( - 'Highlight', - `Highlight ignored because <<${this.client.users.cache.get(user)?.tag ?? user}>> is already mentioned in the message.` - ); - continue; - } - ret.set(user, word); - } - } - - return ret; - } - - /** - * Checks a user provided phrase for their highlights. - * @param guild The guild to check in. - * @param user The user to get the highlights for. - * @param phrase The phrase for highlights in. - * @returns A collection of the user's highlights mapped to weather or not it was matched. - */ - public async checkPhrase(guild: Snowflake, user: Snowflake, phrase: string): Promise<Collection<HighlightWord, boolean>> { - const highlights = await Highlight.findAll({ where: { guild, user } }); - - const results = new Collection<HighlightWord, boolean>(); - - for (const highlight of highlights) { - for (const word of highlight.words) { - results.set(word, this.isMatch(phrase, word)); - } - } - - return results; - } - - /** - * Checks a particular highlight for a match within a phrase. - * @param phrase The phrase to check for the word in. - * @param hl The highlight to check for. - * @returns Whether or not the highlight was matched. - */ - private isMatch(phrase: string, hl: HighlightWord): boolean { - if (hl.regex) { - return new RegExp(hl.word, 'gi').test(phrase); - } else { - if (hl.word.includes(' ')) { - return phrase.toLocaleLowerCase().includes(hl.word.toLocaleLowerCase()); - } else { - const words = phrase.split(/\s*\b\s/); - return words.some((w) => w.toLocaleLowerCase() === hl.word.toLocaleLowerCase()); - } - } - } - - /** - * Adds a new highlight to a user in a particular guild. - * @param guild The guild to add the highlight to. - * @param user The user to add the highlight to. - * @param hl The highlight to add. - * @returns A string representing a user error or a boolean indicating the database success. - */ - public async addHighlight(guild: Snowflake, user: Snowflake, hl: HighlightWord): Promise<string | boolean> { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - if (!guildCache.has(hl)) guildCache.set(hl, new Set()); - guildCache.get(hl)!.add(user); - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (highlight.words.some((w) => w.word === hl.word)) return `You have already highlighted "${hl.word}".`; - - highlight.words = addToArray(highlight.words, hl); - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Removes a highlighted word for a user in a particular guild. - * @param guild The guild to remove the highlight from. - * @param user The user to remove the highlight from. - * @param hl The word to remove. - * @returns A string representing a user error or a boolean indicating the database success. - */ - public async removeHighlight(guild: Snowflake, user: Snowflake, hl: string): Promise<string | boolean> { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - const wordCache = guildCache.find((_, key) => key.word === hl); - - if (!wordCache?.has(user)) return `You have not highlighted "${hl}".`; - - wordCache!.delete(user); - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - const toRemove = highlight.words.find((w) => w.word === hl); - if (!toRemove) return `Uhhhhh... This shouldn't happen.`; - - highlight.words = removeFromArray(highlight.words, toRemove); - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Remove all highlight words for a user in a particular guild. - * @param guild The guild to remove the highlights from. - * @param user The user to remove the highlights from. - * @returns A boolean indicating the database success. - */ - public async removeAllHighlights(guild: Snowflake, user: Snowflake): Promise<boolean> { - if (!this.guildHighlights.has(guild)) this.guildHighlights.set(guild, new Collection()); - const guildCache = this.guildHighlights.get(guild)!; - - for (const [word, users] of guildCache.entries()) { - if (users.has(user)) users.delete(user); - if (users.size === 0) guildCache.delete(word); - } - - const highlight = await Highlight.findOne({ where: { guild, user } }); - - if (!highlight) return false; - - highlight.words = []; - - return Boolean(await highlight.save().catch(() => false)); - } - - /** - * Adds a new user or channel block to a user in a particular guild. - * @param guild The guild to add the block to. - * @param user The user that is blocking the target. - * @param target The target that is being blocked. - * @returns The result of the operation. - */ - public async addBlock( - guild: Snowflake, - user: Snowflake, - target: GuildMember | TextBasedChannel - ): Promise<HighlightBlockResult> { - const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; - const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (highlight[databaseKey].includes(target.id)) return HighlightBlockResult.ALREADY_BLOCKED; - - const newBlocks = addToArray(highlight[databaseKey], target.id); - - highlight[databaseKey] = newBlocks; - const res = await highlight.save().catch(() => false); - if (!res) return HighlightBlockResult.ERROR; - - if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); - const guildBlocks = this[cacheKey].get(guild)!; - guildBlocks.set(user, new Set(newBlocks)); - - return HighlightBlockResult.SUCCESS; - } - - /** - * Removes a user or channel block from a user in a particular guild. - * @param guild The guild to remove the block from. - * @param user The user that is unblocking the target. - * @param target The target that is being unblocked. - * @returns The result of the operation. - */ - public async removeBlock(guild: Snowflake, user: Snowflake, target: GuildMember | Channel): Promise<HighlightUnblockResult> { - const cacheKey = `${target instanceof GuildMember ? 'user' : 'channel'}Blocks` as const; - const databaseKey = `blacklisted${target instanceof GuildMember ? 'Users' : 'Channels'}` as const; - - const [highlight] = await Highlight.findOrCreate({ where: { guild, user } }); - - if (!highlight[databaseKey].includes(target.id)) return HighlightUnblockResult.NOT_BLOCKED; - - const newBlocks = removeFromArray(highlight[databaseKey], target.id); - - highlight[databaseKey] = newBlocks; - const res = await highlight.save().catch(() => false); - if (!res) return HighlightUnblockResult.ERROR; - - if (!this[cacheKey].has(guild)) this[cacheKey].set(guild, new Collection()); - const guildBlocks = this[cacheKey].get(guild)!; - guildBlocks.set(user, new Set(newBlocks)); - - return HighlightUnblockResult.SUCCESS; - } - - /** - * Sends a user a direct message to alert them of their highlight being triggered. - * @param message The message that triggered the highlight. - * @param user The user who's highlights was triggered. - * @param hl The highlight that was matched. - * @returns Whether or a dm was sent. - */ - public async notify(message: Message, user: Snowflake, hl: HighlightWord): Promise<boolean> { - assert(message.inGuild()); - - this.client.console.debug(`Notifying ${user} of highlight ${hl.word} in ${message.guild.name}`); - - dmCooldown: { - const lastDM = this.lastedDMedUserCooldown.get(user); - if (!lastDM?.[0]) break dmCooldown; - - const cooldown = this.client.config.owners.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN; - - if (new Date().getTime() - lastDM[0].createdAt.getTime() < cooldown) { - void this.client.console.verbose('Highlight', `User <<${user}>> has been DMed recently.`); - - if (lastDM[0].embeds.length < 10) { - this.client.console.debug(`Trying to add to notification queue for ${user}`); - return this.addToNotification(lastDM, message, hl); - } - - this.client.console.debug(`User has too many embeds (${lastDM[0].embeds.length}).`); - return false; - } - } - - talkCooldown: { - const lastTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user); - if (!lastTalked) break talkCooldown; - - presence: { - // incase the bot left the guild - if (message.guild) { - const member = message.guild.members.cache.get(user); - if (!member) { - this.client.console.debug(`No member found for ${user} in ${message.guild.name}`); - break presence; - } - - const presence = member.presence ?? (await member.fetch()).presence; - if (!presence) { - this.client.console.debug(`No presence found for ${user} in ${message.guild.name}`); - break presence; - } - - if (presence.status === 'offline') { - void this.client.console.verbose('Highlight', `User <<${user}>> is offline.`); - break talkCooldown; - } - } - } - - const now = new Date().getTime(); - const talked = lastTalked.getTime(); - - if (now - talked < LAST_MESSAGE_COOLDOWN) { - void this.client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`); - - setTimeout(() => { - const newTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user)?.getTime(); - if (talked !== newTalked) return; - - void this.notify(message, user, hl); - }, LAST_MESSAGE_COOLDOWN).unref(); - - return false; - } - } - - return this.client.users - .send(user, { - // eslint-disable-next-line @typescript-eslint/no-base-to-string - content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`, - embeds: [this.generateDmEmbed(message, hl)] - }) - .then((dm) => { - this.lastedDMedUserCooldown.set(user, [dm, message.guildId!, message.channelId, [hl]]); - return true; - }) - .catch(() => false); - } - - private async addToNotification( - [originalDm, guild, channel, originalHl]: lastDmInfo, - message: Message, - hl: HighlightWord - ): Promise<boolean> { - assert(originalDm.embeds.length < 10); - assert(originalDm.embeds.length > 0); - assert(originalDm.channel.type === ChannelType.DM); - this.client.console.debug( - `Adding to notification queue for ${originalDm.channel.recipient?.tag ?? originalDm.channel.recipientId}` - ); - - const sameGuild = guild === message.guildId; - const sameChannel = channel === message.channel.id; - const sameWord = originalHl.every((w) => w.word === hl.word); - - /* eslint-disable @typescript-eslint/no-base-to-string */ - return originalDm - .edit({ - content: `In ${sameGuild ? format.input(message.guild?.name ?? '[Unknown]') : 'multiple servers'} ${ - sameChannel ? message.channel ?? '[Unknown]' : 'multiple channels' - }, ${sameWord ? `your highlight "${hl.word}" was matched:` : 'multiple highlights were matched:'}`, - embeds: [...originalDm.embeds.map((e) => e.toJSON()), this.generateDmEmbed(message, hl)] - }) - .then(() => true) - .catch(() => false); - /* eslint-enable @typescript-eslint/no-base-to-string */ - } - - private generateDmEmbed(message: Message, hl: HighlightWord) { - const recentMessages = message.channel.messages.cache - .filter((m) => m.createdTimestamp <= message.createdTimestamp && m.id !== message.id) - .filter((m) => m.cleanContent?.trim().length > 0) - .sort((a, b) => b.createdTimestamp - a.createdTimestamp) - .first(4) - .reverse(); - - return { - description: [ - // eslint-disable-next-line @typescript-eslint/no-base-to-string - message.channel!.toString(), - ...[...recentMessages, message].map( - (m) => `${timestamp(m.createdAt, 't')} ${format.input(`${m.author.tag}:`)} ${m.cleanContent.trim().substring(0, 512)}` - ) - ].join('\n'), - author: { name: hl.regex ? `/${hl.word}/gi` : hl.word }, - fields: [{ name: 'Source message', value: `[Jump to message](${message.url})` }], - color: colors.default, - footer: { text: `Triggered in ${sanitizeInputForDiscord(`${message.guild}`)}` }, - timestamp: message.createdAt.toISOString() - }; - } - - /** - * Updates the time that a user last talked in a particular guild. - * @param message The message the user sent. - */ - public updateLastTalked(message: Message): void { - if (!message.inGuild()) return; - const lastTalked = ( - this.userLastTalkedCooldown.has(message.guildId) - ? this.userLastTalkedCooldown - : this.userLastTalkedCooldown.set(message.guildId, new Collection()) - ).get(message.guildId)!; - - lastTalked.set(message.author.id, new Date()); - } -} - -export enum HighlightBlockResult { - ALREADY_BLOCKED, - ERROR, - SUCCESS -} - -export enum HighlightUnblockResult { - NOT_BLOCKED, - ERROR, - SUCCESS -} diff --git a/src/lib/common/Sentry.ts b/src/lib/common/Sentry.ts deleted file mode 100644 index 2792203..0000000 --- a/src/lib/common/Sentry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RewriteFrames } from '@sentry/integrations'; -import * as SentryNode from '@sentry/node'; -import { Integrations } from '@sentry/node'; -import type { Config } from '../../../config/Config.js'; - -export class Sentry { - public constructor(rootdir: string, config: Config) { - if (config.credentials.sentryDsn === null) throw TypeError('sentryDsn cannot be null'); - - SentryNode.init({ - dsn: config.credentials.sentryDsn, - environment: config.environment, - tracesSampleRate: 1.0, - integrations: [ - new RewriteFrames({ - root: rootdir - }), - new Integrations.OnUnhandledRejection({ - mode: 'none' - }) - ] - }); - } -} diff --git a/src/lib/common/tags.ts b/src/lib/common/tags.ts deleted file mode 100644 index 098cf29..0000000 --- a/src/lib/common/tags.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* these functions are adapted from the common-tags npm package which is licensed under the MIT license */ -/* the js docs are adapted from the @types/common-tags npm package which is licensed under the MIT license */ - -/** - * Strips the **initial** indentation from the beginning of each line in a multiline string. - */ -export function stripIndent(strings: TemplateStringsArray, ...expressions: any[]) { - const str = format(strings, ...expressions); - // remove the shortest leading indentation from each line - const match = str.match(/^[^\S\n]*(?=\S)/gm); - const indent = match && Math.min(...match.map((el) => el.length)); - if (indent) { - const regexp = new RegExp(`^.{${indent}}`, 'gm'); - return str.replace(regexp, ''); - } - return str; -} - -/** - * Strips **all** of the indentation from the beginning of each line in a multiline string. - */ -export function stripIndents(strings: TemplateStringsArray, ...expressions: any[]) { - const str = format(strings, ...expressions); - // remove all indentation from each line - return str.replace(/^[^\S\n]+/gm, ''); -} - -function format(strings: TemplateStringsArray, ...expressions: any[]) { - const str = strings - .reduce((result, string, index) => ''.concat(result, expressions[index - 1], string)) - .replace(/[^\S\n]+$/gm, '') - .replace(/^\n/, ''); - return str; -} diff --git a/src/lib/common/typings/BushInspectOptions.ts b/src/lib/common/typings/BushInspectOptions.ts deleted file mode 100644 index 30ed01a..0000000 --- a/src/lib/common/typings/BushInspectOptions.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { type InspectOptions } from 'util'; - -/** - * {@link https://nodejs.org/api/util.html#utilinspectobject-showhidden-depth-colors util.inspect Options Documentation} - */ -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; - - /** - * If set to `true`, an underscore is used to separate every three digits in all bigints and numbers. - * - * @default false - */ - numericSeparator?: boolean; - - /** - * Whether or not to inspect strings. - * - * @default false - */ - inspectStrings?: boolean; -} diff --git a/src/lib/common/typings/CodeBlockLang.ts b/src/lib/common/typings/CodeBlockLang.ts deleted file mode 100644 index d0eb4f3..0000000 --- a/src/lib/common/typings/CodeBlockLang.ts +++ /dev/null @@ -1,311 +0,0 @@ -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' - | 'ansi'; diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts deleted file mode 100644 index d362225..0000000 --- a/src/lib/common/util/Arg.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { - type BaseBushArgumentType, - type BushArgumentType, - type BushArgumentTypeCaster, - type CommandMessage, - type SlashMessage -} from '#lib'; -import { Argument, type Command, type Flag, type ParsedValuePredicate } from 'discord-akairo'; -import { type Message } from 'discord.js'; - -/** - * Casts a phrase to this argument's type. - * @param type - The type to cast to. - * @param message - Message that called the command. - * @param phrase - Phrase to process. - */ -export async function cast<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>; -export async function cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>; -export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>; -export async function cast( - this: ThisType<Command>, - type: ATC | AT, - message: CommandMessage | SlashMessage, - phrase: string -): Promise<any> { - return Argument.cast.call(this, type as any, message.client.commandHandler.resolver, message as 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. - */ -export function compose<T extends ATC>(...types: T[]): ATCATCR<T>; -export function compose<T extends KBAT>(...types: T[]): ATCBAT<T>; -export function compose(...types: (AT | ATC)[]): ATC; -export function compose(...types: (AT | ATC)[]): ATC { - return Argument.compose(...(types as any)); -} - -/** - * 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. - */ -export function composeWithFailure<T extends ATC>(...types: T[]): ATCATCR<T>; -export function composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>; -export function composeWithFailure(...types: (AT | ATC)[]): ATC; -export function composeWithFailure(...types: (AT | ATC)[]): ATC { - return Argument.composeWithFailure(...(types as any)); -} - -/** - * Checks if something is null, undefined, or a fail flag. - * @param value - Value to check. - */ -export function 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. - */ -export function product<T extends ATC>(...types: T[]): ATCATCR<T>; -export function product<T extends KBAT>(...types: T[]): ATCBAT<T>; -export function product(...types: (AT | ATC)[]): ATC; -export function product(...types: (AT | ATC)[]): ATC { - return Argument.product(...(types as any)); -} - -/** - * 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. - */ -export function range<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>; -export function range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>; -export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; -export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { - return Argument.range(type as any, 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. - */ -export function tagged<T extends ATC>(type: T, tag?: any): ATCATCR<T>; -export function tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; -export function tagged(type: AT | ATC, tag?: any): ATC; -export function tagged(type: AT | ATC, tag?: any): ATC { - return Argument.tagged(type as any, 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. - */ -export function taggedUnion<T extends ATC>(...types: T[]): ATCATCR<T>; -export function taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>; -export function taggedUnion(...types: (AT | ATC)[]): ATC; -export function taggedUnion(...types: (AT | ATC)[]): ATC { - return Argument.taggedUnion(...(types as any)); -} - -/** - * 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. - */ -export function taggedWithInput<T extends ATC>(type: T, tag?: any): ATCATCR<T>; -export function taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; -export function taggedWithInput(type: AT | ATC, tag?: any): ATC; -export function taggedWithInput(type: AT | ATC, tag?: any): ATC { - return Argument.taggedWithInput(type as any, 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. - */ -export function union<T extends ATC>(...types: T[]): ATCATCR<T>; -export function union<T extends KBAT>(...types: T[]): ATCBAT<T>; -export function union(...types: (AT | ATC)[]): ATC; -export function union(...types: (AT | ATC)[]): ATC { - return Argument.union(...(types as any)); -} - -/** - * 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. - */ -export function validate<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>; -export function validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>; -export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; -export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { - return Argument.validate(type as any, 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. - */ -export function withInput<T extends ATC>(type: T): ATC<ATCR<T>>; -export function withInput<T extends KBAT>(type: T): ATCBAT<T>; -export function withInput(type: AT | ATC): ATC; -export function withInput(type: AT | ATC): ATC { - return Argument.withInput(type as any); -} - -type BushArgumentTypeCasterReturn<R> = R extends BushArgumentTypeCaster<infer S> ? S : R; -/** ```ts - * <R = unknown> = BushArgumentTypeCaster<R> - * ``` */ -type ATC<R = unknown> = BushArgumentTypeCaster<R>; -/** ```ts - * keyof BaseBushArgumentType - * ``` */ -type KBAT = keyof BaseBushArgumentType; -/** ```ts - * <R> = BushArgumentTypeCasterReturn<R> - * ``` */ -type ATCR<R> = BushArgumentTypeCasterReturn<R>; -/** ```ts - * BushArgumentType - * ``` */ -type AT = BushArgumentType; -/** ```ts - * BaseBushArgumentType - * ``` */ -type BAT = BaseBushArgumentType; - -/** ```ts - * <T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>> - * ``` */ -type ATCATCR<T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>>; -/** ```ts - * <T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]> - * ``` */ -type ATCBAT<T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]>; diff --git a/src/lib/common/util/Format.ts b/src/lib/common/util/Format.ts deleted file mode 100644 index debaf4b..0000000 --- a/src/lib/common/util/Format.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { type CodeBlockLang } from '#lib'; -import { - bold as discordBold, - codeBlock as discordCodeBlock, - escapeBold as discordEscapeBold, - escapeCodeBlock as discordEscapeCodeBlock, - escapeInlineCode as discordEscapeInlineCode, - escapeItalic as discordEscapeItalic, - escapeMarkdown, - escapeSpoiler as discordEscapeSpoiler, - escapeStrikethrough as discordEscapeStrikethrough, - escapeUnderline as discordEscapeUnderline, - inlineCode as discordInlineCode, - italic as discordItalic, - spoiler as discordSpoiler, - strikethrough as discordStrikethrough, - underscore as discordUnderscore -} from 'discord.js'; - -/** - * Wraps the content inside a codeblock with no language. - * @param content The content to wrap. - */ -export function 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. - */ -export function codeBlock(language: CodeBlockLang, content: string): string; -export function codeBlock(languageOrContent: string, content?: string): string { - return typeof content === 'undefined' - ? discordCodeBlock(discordEscapeCodeBlock(`${languageOrContent}`)) - : discordCodeBlock(`${languageOrContent}`, discordEscapeCodeBlock(`${content}`)); -} - -/** - * Wraps the content inside \`backticks\`, which formats it as inline code. - * @param content The content to wrap. - */ -export function inlineCode(content: string): string { - return discordInlineCode(discordEscapeInlineCode(`${content}`)); -} - -/** - * Formats the content into italic text. - * @param content The content to wrap. - */ -export function italic(content: string): string { - return discordItalic(discordEscapeItalic(`${content}`)); -} - -/** - * Formats the content into bold text. - * @param content The content to wrap. - */ -export function bold(content: string): string { - return discordBold(discordEscapeBold(`${content}`)); -} - -/** - * Formats the content into underscored text. - * @param content The content to wrap. - */ -export function underscore(content: string): string { - return discordUnderscore(discordEscapeUnderline(`${content}`)); -} - -/** - * Formats the content into strike-through text. - * @param content The content to wrap. - */ -export function strikethrough(content: string): string { - return discordStrikethrough(discordEscapeStrikethrough(`${content}`)); -} - -/** - * Wraps the content inside spoiler (hidden text). - * @param content The content to wrap. - */ -export function spoiler(content: string): string { - return discordSpoiler(discordEscapeSpoiler(`${content}`)); -} - -/** - * Formats input: makes it bold and escapes any other markdown - * @param text The input - */ -export function input(text: string): string { - return bold(sanitizeInputForDiscord(`${text}`)); -} - -/** - * Formats input for logs: makes it highlighted - * @param text The input - */ -export function inputLog(text: string): string { - return `<<${sanitizeWtlAndControl(`${text}`)}>>`; -} - -/** - * Removes all characters in a string that are either control characters or change the direction of text etc. - * @param str The string you would like sanitized - */ -export function sanitizeWtlAndControl(str: string) { - // eslint-disable-next-line no-control-regex - return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, ''); -} - -/** - * Removed wtl and control characters and escapes any other markdown - * @param text The input - */ -export function sanitizeInputForDiscord(text: string): string { - return escapeMarkdown(sanitizeWtlAndControl(`${text}`)); -} - -export { escapeMarkdown } from 'discord.js'; diff --git a/src/lib/common/util/Minecraft.ts b/src/lib/common/util/Minecraft.ts deleted file mode 100644 index a12ebf2..0000000 --- a/src/lib/common/util/Minecraft.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { Byte, Int, parse } from '@ironm00n/nbt-ts'; -import { BitField } from 'discord.js'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export enum FormattingCodes { - Black = '§0', - DarkBlue = '§1', - DarkGreen = '§2', - DarkAqua = '§3', - DarkRed = '§4', - DarkPurple = '§5', - Gold = '§6', - Gray = '§7', - DarkGray = '§8', - Blue = '§9', - Green = '§a', - Aqua = '§b', - Red = '§c', - LightPurple = '§d', - Yellow = '§e', - White = '§f', - - Obfuscated = '§k', - Bold = '§l', - Strikethrough = '§m', - Underline = '§n', - Italic = '§o', - Reset = '§r' -} - -// https://minecraft.fandom.com/wiki/Formatting_codes -export const formattingInfo = { - [FormattingCodes.Black]: { - foreground: 'rgb(0, 0, 0)', - foregroundDarker: 'rgb(0, 0, 0)', - background: 'rgb(0, 0, 0)', - backgroundDarker: 'rgb(0, 0, 0)', - ansi: '\u001b[0;30m' - }, - [FormattingCodes.DarkBlue]: { - foreground: 'rgb(0, 0, 170)', - foregroundDarker: 'rgb(0, 0, 118)', - background: 'rgb(0, 0, 42)', - backgroundDarker: 'rgb(0, 0, 29)', - ansi: '\u001b[0;34m' - }, - [FormattingCodes.DarkGreen]: { - foreground: 'rgb(0, 170, 0)', - foregroundDarker: 'rgb(0, 118, 0)', - background: 'rgb(0, 42, 0)', - backgroundDarker: 'rgb(0, 29, 0)', - ansi: '\u001b[0;32m' - }, - [FormattingCodes.DarkAqua]: { - foreground: 'rgb(0, 170, 170)', - foregroundDarker: 'rgb(0, 118, 118)', - background: 'rgb(0, 42, 42)', - backgroundDarker: 'rgb(0, 29, 29)', - ansi: '\u001b[0;36m' - }, - [FormattingCodes.DarkRed]: { - foreground: 'rgb(170, 0, 0)', - foregroundDarker: 'rgb(118, 0, 0)', - background: 'rgb(42, 0, 0)', - backgroundDarker: 'rgb(29, 0, 0)', - ansi: '\u001b[0;31m' - }, - [FormattingCodes.DarkPurple]: { - foreground: 'rgb(170, 0, 170)', - foregroundDarker: 'rgb(118, 0, 118)', - background: 'rgb(42, 0, 42)', - backgroundDarker: 'rgb(29, 0, 29)', - ansi: '\u001b[0;35m' - }, - [FormattingCodes.Gold]: { - foreground: 'rgb(255, 170, 0)', - foregroundDarker: 'rgb(178, 118, 0)', - background: 'rgb(42, 42, 0)', - backgroundDarker: 'rgb(29, 29, 0)', - ansi: '\u001b[0;33m' - }, - [FormattingCodes.Gray]: { - foreground: 'rgb(170, 170, 170)', - foregroundDarker: 'rgb(118, 118, 118)', - background: 'rgb(42, 42, 42)', - backgroundDarker: 'rgb(29, 29, 29)', - ansi: '\u001b[0;37m' - }, - [FormattingCodes.DarkGray]: { - foreground: 'rgb(85, 85, 85)', - foregroundDarker: 'rgb(59, 59, 59)', - background: 'rgb(21, 21, 21)', - backgroundDarker: 'rgb(14, 14, 14)', - ansi: '\u001b[0;90m' - }, - [FormattingCodes.Blue]: { - foreground: 'rgb(85, 85, 255)', - foregroundDarker: 'rgb(59, 59, 178)', - background: 'rgb(21, 21, 63)', - backgroundDarker: 'rgb(14, 14, 44)', - ansi: '\u001b[0;94m' - }, - [FormattingCodes.Green]: { - foreground: 'rgb(85, 255, 85)', - foregroundDarker: 'rgb(59, 178, 59)', - background: 'rgb(21, 63, 21)', - backgroundDarker: 'rgb(14, 44, 14)', - ansi: '\u001b[0;92m' - }, - [FormattingCodes.Aqua]: { - foreground: 'rgb(85, 255, 255)', - foregroundDarker: 'rgb(59, 178, 178)', - background: 'rgb(21, 63, 63)', - backgroundDarker: 'rgb(14, 44, 44)', - ansi: '\u001b[0;96m' - }, - [FormattingCodes.Red]: { - foreground: 'rgb(255, 85, 85)', - foregroundDarker: 'rgb(178, 59, 59)', - background: 'rgb(63, 21, 21)', - backgroundDarker: 'rgb(44, 14, 14)', - ansi: '\u001b[0;91m' - }, - [FormattingCodes.LightPurple]: { - foreground: 'rgb(255, 85, 255)', - foregroundDarker: 'rgb(178, 59, 178)', - background: 'rgb(63, 21, 63)', - backgroundDarker: 'rgb(44, 14, 44)', - ansi: '\u001b[0;95m' - }, - [FormattingCodes.Yellow]: { - foreground: 'rgb(255, 255, 85)', - foregroundDarker: 'rgb(178, 178, 59)', - background: 'rgb(63, 63, 21)', - backgroundDarker: 'rgb(44, 44, 14)', - ansi: '\u001b[0;93m' - }, - [FormattingCodes.White]: { - foreground: 'rgb(255, 255, 255)', - foregroundDarker: 'rgb(178, 178, 178)', - background: 'rgb(63, 63, 63)', - backgroundDarker: 'rgb(44, 44, 44)', - ansi: '\u001b[0;97m' - }, - - [FormattingCodes.Obfuscated]: { ansi: '\u001b[8m' }, - [FormattingCodes.Bold]: { ansi: '\u001b[1m' }, - [FormattingCodes.Strikethrough]: { ansi: '\u001b[9m' }, - [FormattingCodes.Underline]: { ansi: '\u001b[4m' }, - [FormattingCodes.Italic]: { ansi: '\u001b[3m' }, - [FormattingCodes.Reset]: { ansi: '\u001b[0m' } -} as const; - -export type McItemId = Lowercase<string>; -export type SbItemId = Uppercase<string>; -export type MojangJson = string; -export type SbRecipeItem = `${SbItemId}:${number}` | ''; -export type SbRecipe = { - [Location in `${'A' | 'B' | 'C'}${1 | 2 | 3}`]: SbRecipeItem; -}; -export type InfoType = 'WIKI_URL' | ''; - -export type Slayer = `${'WOLF' | 'BLAZE' | 'EMAN'}_${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`; - -export interface RawNeuItem { - itemid: McItemId; - displayname: string; - nbttag: MojangJson; - damage: number; - lore: string[]; - recipe?: SbRecipe; - internalname: SbItemId; - modver: string; - infoType: InfoType; - info?: string[]; - crafttext: string; - vanilla?: boolean; - useneucraft?: boolean; - slayer_req?: Slayer; - clickcommand?: string; - x?: number; - y?: number; - z?: number; - island?: string; - recipes?: { type: string; cost: any[]; result: SbItemId }[]; - /** @deprecated */ - parent?: SbItemId; - noseal?: boolean; -} - -export enum HideFlagsBits { - Enchantments = 1, - AttributeModifiers = 2, - Unbreakable = 4, - CanDestroy = 8, - CanPlaceOn = 16, - /** - * potion effects, shield pattern info, "StoredEnchantments", written book - * "generation" and "author", "Explosion", "Fireworks", and map tooltips - */ - OtherInformation = 32, - Dyed = 64 -} - -export type HideFlagsString = keyof typeof HideFlagsBits; - -export class HideFlags extends BitField<HideFlagsString> { - public static override Flags = HideFlagsBits; -} - -export const formattingCode = new RegExp( - `§[${Object.values(FormattingCodes) - .filter((v) => v.startsWith('§')) - .map((v) => v.substring(1)) - .join('')}]` -); - -export function removeMCFormatting(str: string) { - return str.replaceAll(formattingCode, ''); -} - -const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous'); - -export interface NbtTag { - overrideMeta?: Byte; - Unbreakable?: Int; - ench?: string[]; - HideFlags?: HideFlags; - SkullOwner?: SkullOwner; - display?: NbtTagDisplay; - ExtraAttributes?: ExtraAttributes; -} - -export interface SkullOwner { - Id?: string; - Properties?: { - textures?: { Value?: string }[]; - }; -} - -export interface NbtTagDisplay { - Lore?: string[]; - color?: Int; - Name?: string; -} - -export type RuneId = string; - -export interface ExtraAttributes { - originTag?: Origin; - id?: string; - generator_tier?: Int; - boss_tier?: Int; - enchantments?: { hardened_mana?: Int }; - dungeon_item_level?: Int; - runes?: { [key: RuneId]: Int }; - petInfo?: PetInfo; -} - -export interface PetInfo { - type: 'ZOMBIE'; - active: boolean; - exp: number; - tier: 'COMMON' | 'UNCOMMON' | 'RARE' | 'EPIC' | 'LEGENDARY'; - hideInfo: boolean; - candyUsed: number; -} - -export type Origin = 'SHOP_PURCHASE'; - -const neuConstantsPath = path.join(repo, 'constants'); -const neuPetsPath = path.join(neuConstantsPath, 'pets.json'); -const neuPets = (await import(neuPetsPath, { assert: { type: 'json' } })) as PetsConstants; -const neuPetNumsPath = path.join(neuConstantsPath, 'petnums.json'); -const neuPetNums = (await import(neuPetNumsPath, { assert: { type: 'json' } })) as PetNums; - -export interface PetsConstants { - pet_rarity_offset: Record<string, number>; - pet_levels: number[]; - custom_pet_leveling: Record<string, { type: number; pet_levels: number[]; max_level: number }>; - pet_types: Record<string, string>; -} - -export interface PetNums { - [key: string]: { - [key: string]: { - '1': { - otherNums: number[]; - statNums: Record<string, number>; - }; - '100': { - otherNums: number[]; - statNums: Record<string, number>; - }; - 'stats_levelling_curve'?: `${number};${number};${number}`; - }; - }; -} - -export class NeuItem { - public itemId: McItemId; - public displayName: string; - public nbtTag: NbtTag; - public internalName: SbItemId; - public lore: string[]; - - public constructor(raw: RawNeuItem) { - this.itemId = raw.itemid; - this.nbtTag = <NbtTag>parse(raw.nbttag); - this.displayName = raw.displayname; - this.internalName = raw.internalname; - this.lore = raw.lore; - - this.petLoreReplacements(); - } - - private petLoreReplacements(level = -1) { - if (/.*?;[0-5]$/.test(this.internalName) && this.displayName.includes('LVL')) { - const maxLevel = neuPets?.custom_pet_leveling?.[this.internalName]?.max_level ?? 100; - this.displayName = this.displayName.replace('LVL', `1➡${maxLevel}`); - - const nums = neuPetNums[this.internalName]; - if (!nums) throw new Error(`Pet (${this.internalName}) has no pet nums.`); - - const teir = ['COMMON', 'UNCOMMON', 'RARE', 'EPIC', 'LEGENDARY', 'MYTHIC'][+this.internalName.at(-1)!]; - const petInfoTier = nums[teir]; - if (!petInfoTier) throw new Error(`Pet (${this.internalName}) has no pet nums for ${teir} rarity.`); - - const curve = petInfoTier?.stats_levelling_curve?.split(';'); - - // todo: finish copying from neu - - const minStatsLevel = parseInt(curve?.[0] ?? '0'); - const maxStatsLevel = parseInt(curve?.[0] ?? '100'); - - const lore = ''; - } - } -} - -export function mcToAnsi(str: string) { - for (const format in formattingInfo) { - str = str.replaceAll(format, formattingInfo[format as keyof typeof formattingInfo].ansi); - } - return `${str}\u001b[0m`; -} diff --git a/src/lib/common/util/Minecraft_Test.ts b/src/lib/common/util/Minecraft_Test.ts deleted file mode 100644 index 26ca648..0000000 --- a/src/lib/common/util/Minecraft_Test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { mcToAnsi, RawNeuItem } from './Minecraft.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const repo = path.join(__dirname, '..', '..', '..', '..', '..', 'neu-item-repo-dangerous'); -const itemPath = path.join(repo, 'items'); -const items = await fs.readdir(itemPath); - -// for (let i = 0; i < 5; i++) { -for (const path_ of items) { - // const randomItem = items[Math.floor(Math.random() * items.length)]; - // console.log(randomItem); - const item = (await import(path.join(itemPath, /* randomItem */ path_), { assert: { type: 'json' } })).default as RawNeuItem; - if (/.*?((_MONSTER)|(_NPC)|(_ANIMAL)|(_MINIBOSS)|(_BOSS)|(_SC))$/.test(item.internalname)) continue; - if (!/.*?;[0-5]$/.test(item.internalname)) continue; - /* console.log(path_); - console.dir(item, { depth: Infinity }); */ - - /* console.log('==========='); */ - // const nbt = parse(item.nbttag) as NbtTag; - - // if (nbt?.SkullOwner?.Properties?.textures?.[0]?.Value) { - // nbt.SkullOwner.Properties.textures[0].Value = parse( - // Buffer.from(nbt.SkullOwner.Properties.textures[0].Value, 'base64').toString('utf-8') - // ) as string; - // } - - // if (nbt.ExtraAttributes?.petInfo) { - // nbt.ExtraAttributes.petInfo = JSON.parse(nbt.ExtraAttributes.petInfo as any as string); - // } - - // delete nbt.display?.Lore; - - // console.dir(nbt, { depth: Infinity }); - // console.log('==========='); - - /* if (nbt?.display && nbt.display.Name !== item.displayname) - console.log(`${path_} display name mismatch: ${mcToAnsi(nbt.display.Name)} != ${mcToAnsi(item.displayname)}`); - - if (nbt?.ExtraAttributes && nbt?.ExtraAttributes.id !== item.internalname) - console.log(`${path_} internal name mismatch: ${mcToAnsi(nbt?.ExtraAttributes.id)} != ${mcToAnsi(item.internalname)}`); */ - - // console.log('==========='); - - console.log(mcToAnsi(item.displayname)); - console.log(item.lore.map((l) => mcToAnsi(l)).join('\n')); - - /* const keys = [ - 'itemid', - 'displayname', - 'nbttag', - 'damage', - 'lore', - 'recipe', - 'internalname', - 'modver', - 'infoType', - 'info', - 'crafttext', - 'vanilla', - 'useneucraft', - 'slayer_req', - 'clickcommand', - 'x', - 'y', - 'z', - 'island', - 'recipes', - 'parent', - 'noseal' - ]; - - Object.keys(item).forEach((k) => { - if (!keys.includes(k)) throw new Error(`Unknown key: ${k}`); - }); - - if ( - 'slayer_req' in item && - !new Array(10).flatMap((_, i) => ['WOLF', 'BLAZE', 'EMAN'].map((e) => e + (i + 1)).includes(item.slayer_req!)) - ) - throw new Error(`Unknown slayer req: ${item.slayer_req!}`); */ - - /* console.log('=-=-=-=-=-=-=-=-=-=-=-=-=-=-\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-'); */ -} diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts deleted file mode 100644 index 60e32c0..0000000 --- a/src/lib/common/util/Moderation.ts +++ /dev/null @@ -1,556 +0,0 @@ -import { - ActivePunishment, - ActivePunishmentType, - baseMuteResponse, - colors, - emojis, - format, - Guild as GuildDB, - humanizeDuration, - ModLog, - permissionsResponse, - type ModLogType, - type ValueOf -} from '#lib'; -import assert from 'assert/strict'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - Client, - EmbedBuilder, - PermissionFlagsBits, - type Guild, - type GuildMember, - type GuildMemberResolvable, - type GuildResolvable, - type Snowflake, - type UserResolvable -} from 'discord.js'; - -enum punishMap { - 'warned' = 'warn', - 'muted' = 'mute', - 'unmuted' = 'unmute', - 'kicked' = 'kick', - 'banned' = 'ban', - 'unbanned' = 'unban', - 'timedout' = 'timeout', - 'untimedout' = 'untimeout', - 'blocked' = 'block', - 'unblocked' = 'unblock' -} -enum reversedPunishMap { - 'warn' = 'warned', - 'mute' = 'muted', - 'unmute' = 'unmuted', - 'kick' = 'kicked', - 'ban' = 'banned', - 'unban' = 'unbanned', - 'timeout' = 'timedout', - 'untimeout' = 'untimedout', - 'block' = 'blocked', - 'unblock' = 'unblocked' -} - -/** - * Checks if a moderator can perform a moderation action on another user. - * @param moderator The person trying to perform the action. - * @param victim The person getting punished. - * @param type The type of punishment - used to format the response. - * @param checkModerator Whether or not to check if the victim is a moderator. - * @param force Override permissions checks. - * @returns `true` if the moderator can perform the action otherwise a reason why they can't. - */ -export async function permissionCheck( - moderator: GuildMember, - victim: GuildMember, - type: - | 'mute' - | 'unmute' - | 'warn' - | 'kick' - | 'ban' - | 'unban' - | 'add a punishment role to' - | 'remove a punishment role from' - | 'block' - | 'unblock' - | 'timeout' - | 'untimeout', - checkModerator = true, - force = false -): Promise<true | string> { - if (force) return true; - - // If the victim is not in the guild anymore it will be undefined - if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true; - - if (moderator.guild.id !== victim.guild.id) { - throw new Error('moderator and victim not in same guild'); - } - - const isOwner = moderator.guild.ownerId === moderator.id; - if (moderator.id === victim.id && !type.startsWith('un')) { - return `${emojis.error} You cannot ${type} yourself.`; - } - if ( - moderator.roles.highest.position <= victim.roles.highest.position && - !isOwner && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`; - } - if ( - victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`; - } - if ( - checkModerator && - victim.permissions.has(PermissionFlagsBits.ManageMessages) && - !(type.startsWith('un') && moderator.id === victim.id) - ) { - if (await moderator.guild.hasFeature('modsCanPunishMods')) { - return true; - } else { - return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`; - } - } - return true; -} - -/** - * Performs permission checks that are required in order to (un)mute a member. - * @param guild The guild to check the mute permissions in. - * @returns A {@link MuteResponse} or true if nothing failed. - */ -export async function checkMutePermissions( - guild: Guild -): Promise<ValueOf<typeof baseMuteResponse> | ValueOf<typeof permissionsResponse> | true> { - if (!guild.members.me!.permissions.has('ManageRoles')) return permissionsResponse.MISSING_PERMISSIONS; - const muteRoleID = await guild.getSetting('muteRole'); - if (!muteRoleID) return baseMuteResponse.NO_MUTE_ROLE; - const muteRole = guild.roles.cache.get(muteRoleID); - if (!muteRole) return baseMuteResponse.MUTE_ROLE_INVALID; - if (muteRole.position >= guild.members.me!.roles.highest.position || muteRole.managed) - return baseMuteResponse.MUTE_ROLE_NOT_MANAGEABLE; - - return true; -} - -/** - * Creates a modlog entry for a punishment. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ -export async function createModLogEntry( - options: CreateModLogEntryOptions, - getCaseNumber = false -): Promise<{ log: ModLog | null; caseNum: number | null }> { - const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; - const moderator = (await options.client.utils.resolveNonCachedUser(options.moderator))!.id; - const guild = options.client.guilds.resolveId(options.guild)!; - - return createModLogEntrySimple( - { - ...options, - user: user, - moderator: moderator, - guild: guild - }, - getCaseNumber - ); -} - -/** - * Creates a modlog entry with already resolved ids. - * @param options Options for creating a modlog entry. - * @param getCaseNumber Whether or not to get the case number of the entry. - * @returns An object with the modlog and the case number. - */ -export async function createModLogEntrySimple( - options: SimpleCreateModLogEntryOptions, - getCaseNumber = false -): Promise<{ log: ModLog | null; caseNum: number | null }> { - // If guild does not exist create it so the modlog can reference a guild. - await GuildDB.findOrCreate({ - where: { id: options.guild }, - defaults: { id: options.guild } - }); - - const modLogEntry = ModLog.build({ - type: options.type, - user: options.user, - moderator: options.moderator, - reason: options.reason, - duration: options.duration ? options.duration : undefined, - guild: options.guild, - pseudo: options.pseudo ?? false, - evidence: options.evidence, - hidden: options.hidden ?? false - }); - const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await options.client.utils.handleError('createModLogEntry', e); - return null; - }); - - if (!getCaseNumber) return { log: saveResult, caseNum: null }; - - const caseNum = ( - await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } }) - )?.length; - return { log: saveResult, caseNum }; -} - -/** - * Creates a punishment entry. - * @param options Options for creating the punishment entry. - * @returns The database entry, or null if no entry is created. - */ -export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> { - const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; - const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; - const guild = options.client.guilds.resolveId(options.guild)!; - const type = findTypeEnum(options.type)!; - - const entry = ActivePunishment.build( - options.extraInfo - ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo } - : { user, type, guild, expires, modlog: options.modlog } - ); - return await entry.save().catch(async (e) => { - await options.client.utils.handleError('createPunishmentEntry', e); - return null; - }); -} - -/** - * Destroys a punishment entry. - * @param options Options for destroying the punishment entry. - * @returns Whether or not the entry was destroyed. - */ -export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> { - const user = await options.client.utils.resolveNonCachedUser(options.user); - const guild = options.client.guilds.resolveId(options.guild); - const type = findTypeEnum(options.type); - - if (!user || !guild) return false; - - let success = true; - - const entries = await ActivePunishment.findAll({ - // finding all cases of a certain type incase there were duplicates or something - where: options.extraInfo - ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } - : { user: user.id, guild: guild, type } - }).catch(async (e) => { - await options.client.utils.handleError('removePunishmentEntry', e); - success = false; - }); - if (entries) { - const promises = entries.map(async (entry) => - entry.destroy().catch(async (e) => { - await options.client.utils.handleError('removePunishmentEntry', e); - success = false; - }) - ); - - await Promise.all(promises); - } - return success; -} - -/** - * Returns the punishment type enum for the given type. - * @param type The type of the punishment. - * @returns The punishment type enum. - */ -function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { - const typeMap = { - ['mute']: ActivePunishmentType.MUTE, - ['ban']: ActivePunishmentType.BAN, - ['role']: ActivePunishmentType.ROLE, - ['block']: ActivePunishmentType.BLOCK - }; - return typeMap[type]; -} - -export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { - return punishMap[punishment]; -} - -export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM { - return reversedPunishMap[punishment]; -} - -/** - * Notifies the specified user of their punishment. - * @param options Options for notifying the user. - * @returns Whether or not the dm was successfully sent. - */ -export async function punishDM(options: PunishDMOptions): Promise<boolean> { - const ending = await options.guild.getSetting('punishmentEnding'); - const dmEmbed = - ending && ending.length && options.sendFooter - ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple) - : undefined; - - const appealsEnabled = !!( - (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals')) - ); - - let content = `You have been ${options.punishment} `; - if (options.punishment.includes('blocked')) { - assert(options.channel); - content += `from <#${options.channel}> `; - } - content += `in ${format.input(options.guild.name)} `; - if (options.duration !== null && options.duration !== undefined) - content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently '; - const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; - content += `for ${format.input(reason)}.`; - - let components; - if (appealsEnabled && options.modlog) - components = [ - new ActionRowBuilder<ButtonBuilder>({ - components: [ - new ButtonBuilder({ - customId: `appeal;${punishmentToPresentTense(options.punishment)};${ - options.guild.id - };${options.client.users.resolveId(options.user)};${options.modlog}`, - style: ButtonStyle.Primary, - label: 'Appeal' - }).toJSON() - ] - }) - ]; - - const dmSuccess = await options.client.users - .send(options.user, { - content, - embeds: dmEmbed ? [dmEmbed] : undefined, - components - }) - .catch(() => false); - return !!dmSuccess; -} - -interface BaseCreateModLogEntryOptions extends BaseOptions { - /** - * The type of modlog entry. - */ - type: ModLogType; - - /** - * The reason for the punishment. - */ - reason: string | undefined | null; - - /** - * The duration of the punishment. - */ - duration?: number; - - /** - * Whether the punishment is a pseudo punishment. - */ - pseudo?: boolean; - - /** - * The evidence for the punishment. - */ - evidence?: string; - - /** - * Makes the modlog entry hidden. - */ - hidden?: boolean; -} - -/** - * Options for creating a modlog entry. - */ -export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { - /** - * The client. - */ - client: Client; - - /** - * The user that a modlog entry is created for. - */ - user: GuildMemberResolvable; - - /** - * The moderator that created the modlog entry. - */ - moderator: GuildMemberResolvable; - - /** - * The guild that the punishment is created for. - */ - guild: GuildResolvable; -} - -/** - * Simple options for creating a modlog entry. - */ -export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOptions { - /** - * The user that a modlog entry is created for. - */ - user: Snowflake; - - /** - * The moderator that created the modlog entry. - */ - moderator: Snowflake; - - /** - * The guild that the punishment is created for. - */ - guild: Snowflake; -} - -/** - * Options for creating a punishment entry. - */ -export interface CreatePunishmentEntryOptions extends BaseOptions { - /** - * The type of punishment. - */ - type: 'mute' | 'ban' | 'role' | 'block'; - - /** - * The user that the punishment is created for. - */ - user: GuildMemberResolvable; - - /** - * The length of time the punishment lasts for. - */ - duration: number | undefined; - - /** - * The guild that the punishment is created for. - */ - guild: GuildResolvable; - - /** - * The id of the modlog that is linked to the punishment entry. - */ - modlog: string; - - /** - * Extra information for the punishment. The role for role punishments and the channel for blocks. - */ - extraInfo?: Snowflake; -} - -/** - * Options for removing a punishment entry. - */ -export interface RemovePunishmentEntryOptions extends BaseOptions { - /** - * The type of punishment. - */ - type: 'mute' | 'ban' | 'role' | 'block'; - - /** - * The user that the punishment is destroyed for. - */ - user: GuildMemberResolvable; - - /** - * The guild that the punishment was in. - */ - guild: GuildResolvable; - - /** - * Extra information for the punishment. The role for role punishments and the channel for blocks. - */ - extraInfo?: Snowflake; -} - -/** - * Options for sending a user a punishment dm. - */ -export interface PunishDMOptions extends BaseOptions { - /** - * The modlog case id so the user can make an appeal. - */ - modlog?: string; - - /** - * The guild that the punishment is taking place in. - */ - guild: Guild; - - /** - * The user that is being punished. - */ - user: UserResolvable; - - /** - * The punishment that the user has received. - */ - punishment: PunishmentTypeDM; - - /** - * The reason the user's punishment. - */ - reason?: string; - - /** - * The duration of the punishment. - */ - duration?: number; - - /** - * Whether or not to send the guild's punishment footer with the dm. - * @default true - */ - sendFooter: boolean; - - /** - * The channel that the user was (un)blocked from. - */ - channel?: Snowflake; -} - -interface BaseOptions { - /** - * The client. - */ - client: Client; -} - -export type PunishmentTypeDM = - | 'warned' - | 'muted' - | 'unmuted' - | 'kicked' - | 'banned' - | 'unbanned' - | 'timedout' - | 'untimedout' - | 'blocked' - | 'unblocked'; - -export type PunishmentTypePresent = - | 'warn' - | 'mute' - | 'unmute' - | 'kick' - | 'ban' - | 'unban' - | 'timeout' - | 'untimeout' - | 'block' - | 'unblock'; - -export type AppealButtonId = `appeal;${PunishmentTypePresent};${Snowflake};${Snowflake};${string}`; |