aboutsummaryrefslogtreecommitdiff
path: root/src/lib/common
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-08-18 22:42:12 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-08-18 22:42:12 -0400
commit2356d2c44736fb83021dacb551625852111c8ce6 (patch)
tree10408d22fdd7a358d2f5c5917c3b59e55aa4c19d /src/lib/common
parent8aed6f93f7740c592cbc0e2f9fd3269c05286077 (diff)
downloadtanzanite-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.ts529
-rw-r--r--src/lib/common/ButtonPaginator.ts219
-rw-r--r--src/lib/common/ConfirmationPrompt.ts64
-rw-r--r--src/lib/common/DeleteButton.ts78
-rw-r--r--src/lib/common/HighlightManager.ts485
-rw-r--r--src/lib/common/Sentry.ts24
-rw-r--r--src/lib/common/tags.ts34
-rw-r--r--src/lib/common/typings/BushInspectOptions.ts123
-rw-r--r--src/lib/common/typings/CodeBlockLang.ts311
-rw-r--r--src/lib/common/util/Arg.ts192
-rw-r--r--src/lib/common/util/Format.ts119
-rw-r--r--src/lib/common/util/Minecraft.ts349
-rw-r--r--src/lib/common/util/Minecraft_Test.ts86
-rw-r--r--src/lib/common/util/Moderation.ts556
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}`;