From 3d0f8d6284fbff51881ba704f73765100ffc5f47 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Sun, 13 Feb 2022 20:39:09 -0500 Subject: started working on appeals --- .eslintrc.json | 22 +- package.json | 26 +- src/arguments/discordEmoji.ts | 4 +- src/commands/config/config.ts | 1 + src/commands/config/features.ts | 2 +- src/commands/dev/servers.ts | 4 +- src/commands/dev/test.ts | 10 +- src/commands/info/guildInfo.ts | 2 +- src/commands/info/help.ts | 3 + src/commands/info/links.ts | 3 + src/commands/info/userInfo.ts | 2 +- src/commands/moulberry-bush/capes.ts | 2 +- src/commands/utilities/highlight-!.ts | 2 +- src/commands/utilities/reminders.ts | 2 +- src/lib/common/AutoMod.ts | 2 + src/lib/common/ButtonPaginator.ts | 7 +- src/lib/common/ConfirmationPrompt.ts | 2 + src/lib/common/DeleteButton.ts | 1 + src/lib/common/util/Moderation.ts | 124 +++++++- .../extensions/discord-akairo/BushClientUtil.ts | 2 +- src/lib/extensions/discord-akairo/BushCommand.ts | 6 +- src/lib/extensions/discord.js/BushGuild.ts | 29 +- src/lib/extensions/discord.js/BushGuildMember.ts | 127 +++++--- src/lib/models/instance/Guild.ts | 9 + src/lib/utils/BushConstants.ts | 4 +- src/listeners/client/interactionCreate.ts | 2 +- .../track-manual-punishments/modlogSyncBan.ts | 2 +- .../track-manual-punishments/modlogSyncKick.ts | 2 +- .../track-manual-punishments/modlogSyncTimeout.ts | 2 +- .../track-manual-punishments/modlogSyncUnban.ts | 2 +- src/listeners/ws/INTERACTION_CREATE.ts | 246 ++++++++++++++- yarn.lock | 337 ++++++++++----------- 32 files changed, 694 insertions(+), 297 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 1d9ffcf..5209b65 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,7 +10,7 @@ "sourceType": "module", "project": "./tsconfig.json" }, - "plugins": ["@typescript-eslint", "deprecation", "node", "import"], + "plugins": ["@typescript-eslint", "deprecation", "import"], "ignorePatterns": ["dist"], "rules": { "no-return-await": "off", @@ -61,8 +61,24 @@ "deprecation/deprecation": "warn", "@typescript-eslint/explicit-member-accessibility": ["warn", { "accessibility": "explicit" }], "@typescript-eslint/switch-exhaustiveness-check": "warn", - "node/file-extension-in-import": ["error", "always", { "tryExtensions": [".js", ".json"] }], "import/no-commonjs": "error", - "import/extensions": ["error", "ignorePackages"] + "import/extensions": ["error", "ignorePackages"], + "@typescript-eslint/no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "discord-api-types", + "message": "Please use discord-api-types/v9 instead.", + "allowTypeImports": true + }, + { + "name": "discord-api-types-next", + "message": "Please use discord-api-types-next/v9 instead.", + "allowTypeImports": true + } + ] + } + ] } } diff --git a/package.json b/package.json index 3489b35..c3ea3e6 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,11 @@ "build:esbuild": "yarn rimraf dist && yarn esbuild --sourcemap=inline --outdir=dist --platform=node --target=es2020 --format=esm --log-level=warning src/**/*.ts", "build:tsc": "yarn rimraf dist && yarn tsc", "build:tsc:no-emit": "yarn rimraf dist && yarn tsc --noEmit", - "_start": "yarn build:esbuild && node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js", - "start": "yarn build:tsc && node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js", + "start:raw": "node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js", + "start:esbuild": "yarn build:esbuild && yarn start:raw", + "start": "yarn build:tsc && yarn start:raw", "start:dry": "yarn start dry", - "dev": "yarn build:tsc && node --enable-source-maps --experimental-json-modules --no-warnings dist/src/bot.js", + "dev": "yarn build:tsc && yarn start:raw", "test": "yarn lint && yarn tsc --noEmit", "format": "yarn prettier . --write", "lint": "yarn eslint --ext js,jsx,ts,tsx src", @@ -55,14 +56,15 @@ "@notenoughupdates/humanize-duration": "^4.0.1", "@notenoughupdates/simplify-number": "^1.0.1", "@notenoughupdates/wolfram-alpha-api": "^1.0.1", - "@sentry/integrations": "^6.17.6", - "@sentry/node": "^6.17.6", - "@sentry/tracing": "^6.17.6", + "@sentry/integrations": "^6.17.7", + "@sentry/node": "^6.17.7", + "@sentry/tracing": "^6.17.7", "canvas": "^2.9.0", "chalk": "^5.0.0", "deep-lock": "^1.0.0", "discord-akairo": "npm:@notenoughupdates/discord-akairo@dev", - "discord-api-types": "0.26.1", + "discord-api-types": "0.27.0", + "discord-api-types-next": "npm:discord-api-types@next", "discord.js": "npm:@notenoughupdates/discord.js@dev", "fuse.js": "^6.5.3", "got": "^12.0.1", @@ -74,7 +76,7 @@ "pg": "^8.7.3", "pg-hstore": "^2.3.4", "prettier": "^2.5.1", - "pretty-bytes": "^5.6.0", + "pretty-bytes": "^6.0.0", "rimraf": "^3.0.2", "sequelize": "6.16.1", "tinycolor2": "^1.4.2", @@ -83,7 +85,7 @@ }, "devDependencies": { "@sapphire/snowflake": "^3.1.0", - "@sentry/types": "^6.17.6", + "@sentry/types": "^6.17.7", "@types/eslint": "^8.4.1", "@types/express": "^4.17.13", "@types/lodash": "^4.14.178", @@ -97,12 +99,14 @@ "@types/validator": "^13.7.1", "@typescript-eslint/eslint-plugin": "^5.11.0", "@typescript-eslint/parser": "^5.11.0", - "eslint": "^8.8.0", + "eslint": "^8.9.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-deprecation": "^1.3.2", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-node": "^11.1.0", "node-fetch": "^3.2.0" }, + "resolutions": { + "@discordjs/rest@npm:^0.4.0-dev": "0.3.0" + }, "packageManager": "yarn@3.1.1" } diff --git a/src/arguments/discordEmoji.ts b/src/arguments/discordEmoji.ts index d9428e1..efaa4dd 100644 --- a/src/arguments/discordEmoji.ts +++ b/src/arguments/discordEmoji.ts @@ -1,5 +1,5 @@ -import { type BushArgumentTypeCaster } from '#lib'; -import { type Snowflake } from 'discord-api-types'; +import type { BushArgumentTypeCaster } from '#lib'; +import type { Snowflake } from 'discord-api-types'; export const discordEmoji: BushArgumentTypeCaster = (_, phrase) => { if (!phrase) return null; diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index 2fae2fd..5346924 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -354,6 +354,7 @@ export default class ConfigCommand extends BushCommand { }; const components = new ActionRow().addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Primary).setCustomId('command_settingsBack').setLabel('Back') ); settingsEmbed.setDescription( diff --git a/src/commands/config/features.ts b/src/commands/config/features.ts index 2e7d623..7fa82a9 100644 --- a/src/commands/config/features.ts +++ b/src/commands/config/features.ts @@ -90,7 +90,7 @@ export default class FeaturesCommand extends BushCommand { .setMaxValues(1) .setMinValues(1) .setOptions( - guildFeatures.map((f) => + ...guildFeatures.map((f) => new SelectMenuOption().setLabel(guildFeaturesObj[f].name).setValue(f).setDescription(guildFeaturesObj[f].description) ) ) diff --git a/src/commands/dev/servers.ts b/src/commands/dev/servers.ts index 173970b..378893e 100644 --- a/src/commands/dev/servers.ts +++ b/src/commands/dev/servers.ts @@ -1,6 +1,6 @@ import { BushCommand, ButtonPaginator, type BushMessage, type BushSlashMessage } from '#lib'; -import { APIEmbed } from 'discord-api-types'; -import { type Guild } from 'discord.js'; +import type { APIEmbed } from 'discord-api-types'; +import type { Guild } from 'discord.js'; export default class ServersCommand extends BushCommand { public constructor() { diff --git a/src/commands/dev/test.ts b/src/commands/dev/test.ts index 669001f..9784412 100644 --- a/src/commands/dev/test.ts +++ b/src/commands/dev/test.ts @@ -1,5 +1,4 @@ import { BushCommand, ButtonPaginator, Shared, type BushMessage } from '#lib'; -// eslint-disable-next-line node/file-extension-in-import import { Routes } from 'discord-api-types/rest/v9'; import { ActionRow, @@ -55,10 +54,15 @@ export default class TestCommand extends BushCommand { if (['button', 'buttons'].includes(args?.feature?.toLowerCase())) { const ButtonRow = new ActionRow().addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Primary).setCustomId('primaryButton').setLabel('Primary'), + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Secondary).setCustomId('secondaryButton').setLabel('Secondary'), + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Success).setCustomId('successButton').setLabel('Success'), + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Danger).setCustomId('dangerButton').setLabel('Danger'), + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Link').setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ') ); return await message.util.reply({ content: 'buttons', components: [ButtonRow] }); @@ -78,6 +82,7 @@ export default class TestCommand extends BushCommand { .setTitle('Title'); const buttonRow = new ActionRow().addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Link').setURL('https://google.com/') ); return await message.util.reply({ content: 'Test', embeds: [embed], components: [buttonRow] }); @@ -87,6 +92,7 @@ export default class TestCommand extends BushCommand { const row = new ActionRow(); for (let b = 1; b <= 5; b++) { const id = (a + 5 * (b - 1)).toString(); + // @ts-expect-error: outdated @discord.js/builders const button = new ButtonComponent().setStyle(ButtonStyle.Primary).setCustomId(id).setLabel(id); row.addComponents(button); } @@ -119,6 +125,7 @@ export default class TestCommand extends BushCommand { const row = new ActionRow(); for (let b = 1; b <= 5; b++) { const id = (a + 5 * (b - 1)).toString(); + // @ts-expect-error: outdated @discord.js/builders const button = new ButtonComponent().setStyle(ButtonStyle.Secondary).setCustomId(id).setLabel(id); row.addComponents(button); } @@ -151,6 +158,7 @@ export default class TestCommand extends BushCommand { content: 'Click for modal', components: [ new ActionRow().addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Primary).setLabel('Modal').setCustomId('test;modal') ) ] diff --git a/src/commands/info/guildInfo.ts b/src/commands/info/guildInfo.ts index a4b7fb9..ed8a973 100644 --- a/src/commands/info/guildInfo.ts +++ b/src/commands/info/guildInfo.ts @@ -1,6 +1,6 @@ import { BushCommand, type ArgType, type BushMessage, type BushSlashMessage, type OptionalArgType } from '#lib'; import assert from 'assert'; -import { GuildDefaultMessageNotifications, GuildExplicitContentFilter } from 'discord-api-types'; +import { GuildDefaultMessageNotifications, GuildExplicitContentFilter } from 'discord-api-types/v9'; import { ApplicationCommandOptionType, Embed, diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 2383566..ef3ef30 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -143,14 +143,17 @@ export default class HelpCommand extends BushCommand { const row = new ActionRow(); if (!client.config.isDevelopment && !client.guilds.cache.some((guild) => guild.ownerId === message.author.id)) { + // @ts-expect-error: outdated @discord.js/builders row.addComponents(new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Invite Me').setURL(util.invite)); } if (!client.guilds.cache.get(client.config.supportGuild.id)?.members.cache.has(message.author.id)) { row.addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Support Server').setURL(client.config.supportGuild.invite) ); } if (packageDotJSON?.repository) + // @ts-expect-error: outdated @discord.js/builders row.addComponents(new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('GitHub').setURL(packageDotJSON.repository)); else void message.channel?.send('Error importing package.json, please report this to my developer.'); diff --git a/src/commands/info/links.ts b/src/commands/info/links.ts index 25b040c..d9d5b8a 100644 --- a/src/commands/info/links.ts +++ b/src/commands/info/links.ts @@ -22,10 +22,13 @@ export default class LinksCommand extends BushCommand { public override async exec(message: BushMessage | BushSlashMessage) { const buttonRow = new ActionRow(); if (!client.config.isDevelopment || message.author.isOwner()) { + // @ts-expect-error: outdated @discord.js/builders buttonRow.addComponents(new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Invite Me').setURL(util.invite)); } buttonRow.addComponents( + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('Support Server').setURL(client.config.supportGuild.invite), + // @ts-expect-error: outdated @discord.js/builders new ButtonComponent().setStyle(ButtonStyle.Link).setLabel('GitHub').setURL(packageDotJSON.repository) ); return await message.util.reply({ content: 'Here are some useful links:', components: [buttonRow] }); diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts index 97cdc30..02a3be6 100644 --- a/src/commands/info/userInfo.ts +++ b/src/commands/info/userInfo.ts @@ -7,7 +7,7 @@ import { type BushSlashMessage, type BushUser } from '#lib'; -import { APIApplication, TeamMemberMembershipState } from 'discord-api-types'; +import { TeamMemberMembershipState, type APIApplication } from 'discord-api-types/v9'; import { ActivityType, ApplicationCommandOptionType, diff --git a/src/commands/moulberry-bush/capes.ts b/src/commands/moulberry-bush/capes.ts index 032f62d..47a4ea6 100644 --- a/src/commands/moulberry-bush/capes.ts +++ b/src/commands/moulberry-bush/capes.ts @@ -1,6 +1,6 @@ import { BushCommand, ButtonPaginator, DeleteButton, type BushMessage, type OptionalArgType } from '#lib'; import assert from 'assert'; -import { APIEmbed } from 'discord-api-types'; +import { APIEmbed } from 'discord-api-types/v9'; import { ApplicationCommandOptionType, AutocompleteInteraction, PermissionFlagsBits } from 'discord.js'; import Fuse from 'fuse.js'; import got from 'got'; diff --git a/src/commands/utilities/highlight-!.ts b/src/commands/utilities/highlight-!.ts index 332af03..687990f 100644 --- a/src/commands/utilities/highlight-!.ts +++ b/src/commands/utilities/highlight-!.ts @@ -1,6 +1,6 @@ import { BushCommand, Highlight, HighlightWord, type BushSlashMessage } from '#lib'; import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo'; -import { ApplicationCommandOptionType } from 'discord-api-types'; +import { ApplicationCommandOptionType } from 'discord-api-types/v9'; import { ApplicationCommandSubCommandData, AutocompleteInteraction, CacheType } from 'discord.js'; type Unpacked = T extends (infer U)[] ? U : T; diff --git a/src/commands/utilities/reminders.ts b/src/commands/utilities/reminders.ts index 509da67..10206c1 100644 --- a/src/commands/utilities/reminders.ts +++ b/src/commands/utilities/reminders.ts @@ -1,6 +1,6 @@ import { BushCommand, ButtonPaginator, Reminder, type BushMessage, type BushSlashMessage } from '#lib'; import assert from 'assert'; -import { APIEmbed } from 'discord-api-types'; +import { APIEmbed } from 'discord-api-types/v9'; import { PermissionFlagsBits } from 'discord.js'; import { Op } from 'sequelize'; diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index 784085d..9024260 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -156,6 +156,7 @@ export class AutoMod { ? [ new ActionRow().addComponents( new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Danger) .setLabel('Ban User') .setCustomId(`automod;ban;${this.message.author.id};everyone mention and scam phrase`) @@ -277,6 +278,7 @@ export class AutoMod { ? [ new ActionRow().addComponents( new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Danger) .setLabel('Ban User') .setCustomId(`automod;ban;${this.message.author.id};${highestOffence.reason}`) diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts index 0399e74..09e059c 100644 --- a/src/lib/common/ButtonPaginator.ts +++ b/src/lib/common/ButtonPaginator.ts @@ -1,6 +1,6 @@ import { DeleteButton, type BushMessage, type BushSlashMessage } from '#lib'; import { CommandUtil } from 'discord-akairo'; -import { APIEmbed } from 'discord-api-types'; +import { APIEmbed } from 'discord-api-types/v9'; import { ActionRow, ActionRowComponent, ButtonComponent, ButtonStyle, Embed, type MessageComponentInteraction } from 'discord.js'; /** @@ -173,26 +173,31 @@ export class ButtonPaginator { protected getPaginationRow(disableAll = false): ActionRow { return new ActionRow().addComponents( new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate_beginning') .setEmoji(PaginateEmojis.BEGINNING) .setDisabled(disableAll || this.curPage === 0), new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate_back') .setEmoji(PaginateEmojis.BACK) .setDisabled(disableAll || this.curPage === 0), new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate_stop') .setEmoji(PaginateEmojis.STOP) .setDisabled(disableAll), new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate_next') .setEmoji(PaginateEmojis.FORWARD) .setDisabled(disableAll || this.curPage === this.embeds.length - 1), new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate_end') .setEmoji(PaginateEmojis.END) diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts index bd11c5c..1f027ef 100644 --- a/src/lib/common/ConfirmationPrompt.ts +++ b/src/lib/common/ConfirmationPrompt.ts @@ -31,11 +31,13 @@ export class ConfirmationPrompt { this.messageOptions.components = [ new ActionRow().addComponents( new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('confirmationPrompt_confirm') .setEmoji({ id: util.emojisRaw.successFull, name: 'successFull', animated: false }) .setLabel('Yes'), new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Danger) .setCustomId('confirmationPrompt_cancel') .setEmoji({ id: util.emojisRaw.errorFull, name: 'errorFull', animated: false }) diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts index cf3b416..f2e0ff3 100644 --- a/src/lib/common/DeleteButton.ts +++ b/src/lib/common/DeleteButton.ts @@ -68,6 +68,7 @@ export class DeleteButton { this.messageOptions.components = [ new ActionRow().addComponents( new ButtonComponent() + // @ts-expect-error: outdated @discord.js/builders .setStyle(ButtonStyle.Primary) .setCustomId('paginate__stop') .setEmoji(PaginateEmojis.STOP) diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts index 0ba6fca..c2236ab 100644 --- a/src/lib/common/util/Moderation.ts +++ b/src/lib/common/util/Moderation.ts @@ -10,7 +10,33 @@ import { type BushUserResolvable, type ModLogType } from '#lib'; -import { Embed, PermissionFlagsBits, type Snowflake } from 'discord.js'; +import assert from 'assert'; +import { ActionRow, ButtonComponent, ButtonStyle, ComponentType, Embed, PermissionFlagsBits, type Snowflake } 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' +} /** * A utility class with moderation-related methods. @@ -204,6 +230,19 @@ export class Moderation { return typeMap[type]; } + public static punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent { + return punishMap[punishment]; + } + + public static 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. + */ public static async punishDM(options: PunishDMOptions): Promise { const ending = await options.guild.getSetting('punishmentEnding'); const dmEmbed = @@ -211,16 +250,45 @@ export class Moderation { ? new Embed().setDescription(ending).setColor(util.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 ${util.format.input(options.guild.name)} `; + if (options.duration !== null && options.duration !== undefined) + content += options.duration ? `for ${util.humanizeDuration(options.duration)} ` : 'permanently '; + const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided'; + content += `for ${util.format.input(reason)}.`; + + let components; + if (appealsEnabled && options.modlog) + components = [ + new ActionRow({ + type: ComponentType.ActionRow, + components: [ + // @ts-expect-error: outdated @discord.js/builders + new ButtonComponent({ + custom_id: `appeal;${this.punishmentToPresentTense(options.punishment)};${ + options.guild.id + };${client.users.resolveId(options.user)};${options.modlog}`, + style: ButtonStyle.Primary, + type: ComponentType.Button, + label: 'Appeal' + }) + ] + }) + ]; + const dmSuccess = await client.users .send(options.user, { - content: `You have been ${options.punishment} in **${options.guild.name}** ${ - options.duration !== null && options.duration !== undefined - ? options.duration - ? `for ${util.humanizeDuration(options.duration)} ` - : 'permanently ' - : '' - }for **${options.reason?.trim() ? options.reason?.trim() : 'No reason provided'}**.`, - embeds: dmEmbed ? [dmEmbed] : undefined + content, + embeds: dmEmbed ? [dmEmbed] : undefined, + components }) .catch(() => false); return !!dmSuccess; @@ -341,6 +409,11 @@ export interface RemovePunishmentEntryOptions { * Options for sending a user a punishment dm. */ export interface PunishDMOptions { + /** + * The modlog case id so the user can make an appeal. + */ + modlog?: string; + /** * The guild that the punishment is taking place in. */ @@ -354,7 +427,7 @@ export interface PunishDMOptions { /** * The punishment that the user has received. */ - punishment: string; + punishment: PunishmentTypeDM; /** * The reason the user's punishment. @@ -371,4 +444,35 @@ export interface PunishDMOptions { * @default true */ sendFooter: boolean; + + /** + * The channel that the user was (un)blocked from. + */ + channel?: Snowflake; } + +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}`; diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index 41d16f7..bf4dfaf 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -21,7 +21,7 @@ import assert from 'assert'; import { exec } from 'child_process'; import deepLock from 'deep-lock'; import { ClientUtil, Util as AkairoUtil } from 'discord-akairo'; -import { APIMessage } from 'discord-api-types'; +import type { APIMessage } from 'discord-api-types/v9'; import { Constants as DiscordConstants, GuildMember, diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 650b538..ff3748e 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -44,7 +44,7 @@ import { type ContextMenuCommand, type MissingPermissionSupplier, type SlashOption, - type SlashResolveTypes + type SlashResolveType } from 'discord-akairo'; import { type ApplicationCommandOptionChoice, @@ -147,7 +147,7 @@ interface BaseBushArgumentOptions extends Omit { + // add modlog entry + const { log: modlog } = await Moderation.createModLogEntry({ + type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, + user: user, + moderator: moderator.id, + reason: options.reason, + duration: options.duration, + guild: this, + evidence: options.evidence + }); + if (!modlog) return banResponse.MODLOG_ERROR; + caseID = modlog.id; + // dm user dmSuccessEvent = await Moderation.punishDM({ + modlog: modlog.id, guild: this, user: user, punishment: 'banned', @@ -187,24 +201,11 @@ export class BushGuild extends Guild { const banSuccess = await this.bans .create(user?.id ?? options.user, { reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - days: options.deleteDays + deleteMessageDays: options.deleteDays }) .catch(() => false); if (!banSuccess) return banResponse.ACTION_ERROR; - // add modlog entry - const { log: modlog } = await Moderation.createModLogEntry({ - type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, - user: user, - moderator: moderator.id, - reason: options.reason, - duration: options.duration, - guild: this, - evidence: options.evidence - }); - if (!modlog) return banResponse.MODLOG_ERROR; - caseID = modlog.id; - // add punishment entry so they can be unbanned later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'ban', diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index 84fdf13..5d7144b 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -3,6 +3,8 @@ import { BushClientEvents, Moderation, ModLogType, + PunishmentTypeDM, + Time, type BushClient, type BushGuild, type BushGuildTextBasedChannel, @@ -29,14 +31,29 @@ export class BushGuildMember extends GuildMember { /** * Send a punishment dm to the user. + * @param modlog The modlog case id so the user can make an appeal. * @param punishment The punishment that the user has received. * @param reason The reason for the user's punishment. * @param duration The duration of the punishment. * @param sendFooter Whether or not to send the guild's punishment footer with the dm. * @returns Whether or not the dm was sent successfully. */ - public async bushPunishDM(punishment: string, reason?: string | null, duration?: number, sendFooter = true): Promise { - return Moderation.punishDM({ guild: this.guild, user: this, punishment, reason: reason ?? undefined, duration, sendFooter }); + public async bushPunishDM( + punishment: PunishmentTypeDM, + reason?: string | null, + duration?: number, + modlog?: string, + sendFooter = true + ): Promise { + return Moderation.punishDM({ + modlog, + guild: this.guild, + user: this, + punishment, + reason: reason ?? undefined, + duration, + sendFooter + }); } /** @@ -304,7 +321,7 @@ export class BushGuildMember extends GuildMember { if (!options.silent) { // dm user - const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0); + const dmSuccess = await this.bushPunishDM('muted', options.reason, options.duration ?? 0, modlog.id); dmSuccessEvent = dmSuccess; if (!dmSuccess) return muteResponse.DM_ERROR; } @@ -386,7 +403,7 @@ export class BushGuildMember extends GuildMember { if (!options.silent) { // dm user - const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, false); + const dmSuccess = await this.bushPunishDM('unmuted', options.reason, undefined, '', false); dmSuccessEvent = dmSuccess; if (!dmSuccess) return unmuteResponse.DM_ERROR; } @@ -429,14 +446,6 @@ export class BushGuildMember extends GuildMember { const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.me); if (!moderator) return kickResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { - // dm user - const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason); - dmSuccessEvent = dmSuccess ?? undefined; - - // kick - const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); - if (!kickSuccess) return kickResponse.ACTION_ERROR; - // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ type: ModLogType.KICK, @@ -449,6 +458,15 @@ export class BushGuildMember extends GuildMember { }); if (!modlog) return kickResponse.MODLOG_ERROR; caseID = modlog.id; + + // dm user + const dmSuccess = options.silent ? null : await this.bushPunishDM('kicked', options.reason, undefined, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // kick + const kickSuccess = await this.kick(`${moderator?.tag} | ${options.reason ?? 'No reason provided.'}`).catch(() => false); + if (!kickSuccess) return kickResponse.ACTION_ERROR; + if (dmSuccess === false) return kickResponse.DM_ERROR; return kickResponse.SUCCESS; })(); @@ -489,17 +507,6 @@ export class BushGuildMember extends GuildMember { }); const ret = await (async () => { - // dm user - const dmSuccess = options.silent ? null : await this.bushPunishDM('banned', options.reason, options.duration ?? 0); - dmSuccessEvent = dmSuccess ?? undefined; - - // ban - const banSuccess = await this.ban({ - reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, - days: options.deleteDays - }).catch(() => false); - if (!banSuccess) return banResponse.ACTION_ERROR; - // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, @@ -514,6 +521,19 @@ export class BushGuildMember extends GuildMember { if (!modlog) return banResponse.MODLOG_ERROR; caseID = modlog.id; + // dm user + const dmSuccess = options.silent + ? null + : await this.bushPunishDM('banned', options.reason, options.duration ?? 0, modlog.id); + dmSuccessEvent = dmSuccess ?? undefined; + + // ban + const banSuccess = await this.ban({ + reason: `${moderator.tag} | ${options.reason ?? 'No reason provided.'}`, + deleteMessageDays: options.deleteDays + }).catch(() => false); + if (!banSuccess) return banResponse.ACTION_ERROR; + // add punishment entry so they can be unbanned later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ type: 'ban', @@ -595,20 +615,21 @@ export class BushGuildMember extends GuildMember { }); if (!punishmentEntrySuccess) return blockResponse.PUNISHMENT_ENTRY_ADD_ERROR; - if (!options.silent) { - // dm user - const dmSuccess = await this.send({ - content: `You have been blocked from <#${channel.id}> in **${this.guild.name}** ${ - options.duration !== null && options.duration !== undefined - ? options.duration - ? `for ${util.humanizeDuration(options.duration)} ` - : 'permanently ' - : '' - }for **${options.reason?.trim() ? options.reason?.trim() : 'No reason provided'}**.` - }).catch(() => false); - dmSuccessEvent = !!dmSuccess; - if (!dmSuccess) return blockResponse.DM_ERROR; - } + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + punishment: 'blocked', + reason: options.reason ?? undefined, + duration: options.duration ?? 0, + modlog: modlog.id, + guild: this.guild, + user: this, + sendFooter: true, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; return blockResponse.SUCCESS; })(); @@ -683,16 +704,22 @@ export class BushGuildMember extends GuildMember { }); if (!punishmentEntrySuccess) return unblockResponse.ACTION_ERROR; - if (!options.silent) { - // dm user - const dmSuccess = await this.send({ - content: `You have been unblocked from <#${channel.id}> in **${this.guild.name}** for **${ - options.reason?.trim() ? options.reason?.trim() : 'No reason provided' - }**.` - }).catch(() => false); - dmSuccessEvent = !!dmSuccess; - if (!dmSuccess) return unblockResponse.DM_ERROR; - } + // dm user + const dmSuccess = options.silent + ? null + : await Moderation.punishDM({ + punishment: 'unblocked', + reason: options.reason ?? undefined, + guild: this.guild, + user: this, + sendFooter: false, + channel: channel.id + }); + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return blockResponse.DM_ERROR; + + dmSuccessEvent = !!dmSuccess; + if (!dmSuccess) return unblockResponse.DM_ERROR; return unblockResponse.SUCCESS; })(); @@ -723,7 +750,7 @@ export class BushGuildMember extends GuildMember { // checks if (!this.guild.me!.permissions.has(PermissionFlagsBits.ModerateMembers)) return timeoutResponse.MISSING_PERMISSIONS; - const twentyEightDays = client.consts.timeUnits.days.value * 28; + const twentyEightDays = Time.Day * 28; if (options.duration > twentyEightDays) return timeoutResponse.INVALID_DURATION; let caseID: string | undefined = undefined; @@ -756,7 +783,7 @@ export class BushGuildMember extends GuildMember { if (!options.silent) { // dm user - const dmSuccess = await this.bushPunishDM('timed out', options.reason, options.duration); + const dmSuccess = await this.bushPunishDM('timedout', options.reason, options.duration, modlog.id); dmSuccessEvent = dmSuccess; if (!dmSuccess) return timeoutResponse.DM_ERROR; } @@ -815,7 +842,7 @@ export class BushGuildMember extends GuildMember { if (!options.silent) { // dm user - const dmSuccess = await this.bushPunishDM('untimedout', options.reason); + const dmSuccess = await this.bushPunishDM('untimedout', options.reason, undefined, '', false); dmSuccessEvent = dmSuccess; if (!dmSuccess) return removeTimeoutResponse.DM_ERROR; } diff --git a/src/lib/models/instance/Guild.ts b/src/lib/models/instance/Guild.ts index b41eb9e..b81562c 100644 --- a/src/lib/models/instance/Guild.ts +++ b/src/lib/models/instance/Guild.ts @@ -385,6 +385,11 @@ export const guildFeaturesObj = asGuildFeature({ name: 'Log Manual Punishments', description: "Adds manual punishment to the user's modlogs and the logging channels.", default: true + }, + punishmentAppeals: { + name: 'Punishment Appeals', + description: 'Allow users to appeal their punishments and send the appeal to the configured channel.', + default: false } }); @@ -404,6 +409,10 @@ export const guildLogsObj = { error: { description: 'Logs errors that occur with the bot.', configurable: true + }, + appeals: { + description: 'Where punishment appeals are sent.', + configurable: true } }; diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index 4327fec..93de100 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -317,7 +317,6 @@ export class BushConstants { }, userFlags: { - None: '', Staff: '<:discordEmployee:848742947826434079>', Partner: '<:partneredServerOwner:848743051593777152>', Hypesquad: '<:hypeSquadEvents:848743108283072553>', @@ -331,7 +330,8 @@ export class BushConstants { VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>', VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>', CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>', - BotHTTPInteractions: 'BotHTTPInteractions' + BotHTTPInteractions: 'BotHTTPInteractions', + Spammer: 'Spammer' }, status: { diff --git a/src/listeners/client/interactionCreate.ts b/src/listeners/client/interactionCreate.ts index 636bb6e..40315a0 100644 --- a/src/listeners/client/interactionCreate.ts +++ b/src/listeners/client/interactionCreate.ts @@ -20,7 +20,7 @@ export default class InteractionCreateListener extends BushListener { return; } else if (interaction.isButton()) { const id = interaction.customId; - if (id.startsWith('paginate_') || id.startsWith('command_') || id.startsWith('confirmationPrompt_')) return; + if (['paginate_', 'command_', 'confirmationPrompt_', 'appeal'].some((s) => id.startsWith(s))) return; else if (id.startsWith('automod;')) void AutoMod.handleInteraction(interaction as BushButtonInteraction); else return await interaction.reply({ content: 'Buttons go brrr', ephemeral: true }); } else if (interaction.isSelectMenu()) { diff --git a/src/listeners/track-manual-punishments/modlogSyncBan.ts b/src/listeners/track-manual-punishments/modlogSyncBan.ts index 9886530..b68de7c 100644 --- a/src/listeners/track-manual-punishments/modlogSyncBan.ts +++ b/src/listeners/track-manual-punishments/modlogSyncBan.ts @@ -1,5 +1,5 @@ import { BushListener, BushUser, Moderation, ModLogType, Time, type BushClientEvents } from '#lib'; -import { AuditLogEvent } from 'discord-api-types'; +import { AuditLogEvent } from 'discord-api-types/v9'; import { Embed, PermissionFlagsBits } from 'discord.js'; export default class ModlogSyncBanListener extends BushListener { diff --git a/src/listeners/track-manual-punishments/modlogSyncKick.ts b/src/listeners/track-manual-punishments/modlogSyncKick.ts index b7762db..6ff9bd6 100644 --- a/src/listeners/track-manual-punishments/modlogSyncKick.ts +++ b/src/listeners/track-manual-punishments/modlogSyncKick.ts @@ -1,5 +1,5 @@ import { BushListener, BushUser, Moderation, ModLogType, Time, type BushClientEvents } from '#lib'; -import { AuditLogEvent } from 'discord-api-types'; +import { AuditLogEvent } from 'discord-api-types/v9'; import { Embed, PermissionFlagsBits } from 'discord.js'; export default class ModlogSyncKickListener extends BushListener { diff --git a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts index 21dde1a..993002e 100644 --- a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts +++ b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts @@ -1,5 +1,5 @@ import { BushListener, BushUser, Moderation, ModLogType, Time, type BushClientEvents } from '#lib'; -import { AuditLogEvent } from 'discord-api-types'; +import { AuditLogEvent } from 'discord-api-types/v9'; import { Embed, PermissionFlagsBits } from 'discord.js'; export default class ModlogSyncTimeoutListener extends BushListener { diff --git a/src/listeners/track-manual-punishments/modlogSyncUnban.ts b/src/listeners/track-manual-punishments/modlogSyncUnban.ts index a268ef4..366d072 100644 --- a/src/listeners/track-manual-punishments/modlogSyncUnban.ts +++ b/src/listeners/track-manual-punishments/modlogSyncUnban.ts @@ -1,5 +1,5 @@ import { BushListener, BushUser, Moderation, ModLogType, Time, type BushClientEvents } from '#lib'; -import { AuditLogEvent } from 'discord-api-types'; +import { AuditLogEvent } from 'discord-api-types/v9'; import { Embed, PermissionFlagsBits } from 'discord.js'; export default class ModlogSyncUnbanListener extends BushListener { diff --git a/src/listeners/ws/INTERACTION_CREATE.ts b/src/listeners/ws/INTERACTION_CREATE.ts index a7c8a45..fd79529 100644 --- a/src/listeners/ws/INTERACTION_CREATE.ts +++ b/src/listeners/ws/INTERACTION_CREATE.ts @@ -1,6 +1,29 @@ -import { BushListener } from '#lib'; -// eslint-disable-next-line node/file-extension-in-import -import { GatewayDispatchEvents, Routes } from 'discord-api-types/v9'; +import { BushListener, BushUser, Moderation, ModLog, PunishmentTypePresent } from '#lib'; +import assert from 'assert'; +import { TextInputStyle } from 'discord-api-types-next/v9'; +import { + APIBaseInteraction, + APIEmbed, + APIInteraction as DiscordAPITypesAPIInteraction, + APIInteractionResponseChannelMessageWithSource, + APIInteractionResponseDeferredMessageUpdate, + APIInteractionResponseUpdateMessage, + APIModalInteractionResponse, + APIModalSubmission, + ButtonStyle, + ComponentType, + GatewayDispatchEvents, + InteractionResponseType, + InteractionType, + Routes +} from 'discord-api-types/v9'; +import { ActionRow, ButtonComponent, Embed, Snowflake } from 'discord.js'; + +// todo: use from discord-api-types once updated +export type APIModalSubmitInteraction = APIBaseInteraction & + Required, 'data'>>; + +export type APIInteraction = DiscordAPITypesAPIInteraction | APIModalSubmitInteraction; export default class WsInteractionCreateListener extends BushListener { public constructor() { @@ -11,15 +34,218 @@ export default class WsInteractionCreateListener extends BushListener { }); } - public override async exec(interaction: any) { - // console.dir(interaction); + public override async exec(interaction: APIInteraction) { + console.dir(interaction); - if (interaction.type === 5) { - await this.client.rest.post(Routes.interactionCallback(interaction.id, interaction.token), { - body: { - type: 6 - } + const respond = ( + options: + | APIModalInteractionResponse + | APIInteractionResponseDeferredMessageUpdate + | APIInteractionResponseChannelMessageWithSource + | APIInteractionResponseUpdateMessage + ) => { + return this.client.rest.post( + Routes.interactionCallback(interaction.id, interaction.token), + options ? { body: options } : undefined + ); + }; + + const deferredMessageUpdate = () => { + return respond({ + type: InteractionResponseType.DeferredMessageUpdate }); + }; + + if (interaction.type === InteractionType.MessageComponent) { + if (interaction.data.custom_id.startsWith('appeal;')) { + const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [ + 'appeal', + PunishmentTypePresent, + Snowflake, + Snowflake, + string + ]; + + const guild = client.guilds.resolve(guildId); + if (!guild) + return respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `${util.emojis.error} I am no longer in that server.` + } + }); + + const modal: APIModalInteractionResponse = { + type: InteractionResponseType.Modal, + data: { + custom_id: `appeal_submit;${punishment};${guildId};${userId};${modlogCase}`, + title: `${util.capitalize(punishment)} Appeal`, + components: [ + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + style: TextInputStyle.Paragraph, + max_length: 1024, + required: true, + label: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`, + placeholder: `Why do you think you received a ${punishment}?`, + custom_id: 'appeal_reason' + } + ] + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + style: TextInputStyle.Paragraph, + max_length: 1024, + required: true, + label: 'Do you believe it was fair?', + placeholder: `Why do you think you received a ${punishment}?`, + custom_id: 'appeal_fair' + } + ] + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + style: TextInputStyle.Paragraph, + max_length: 1024, + required: true, + label: `Why should your ${punishment} be removed?`, + placeholder: `Why should your ${punishment} be removed?`, + custom_id: 'appeal_why' + } + ] + } + ] + } + }; + + return respond(modal); + } else if ( + interaction.data.custom_id.startsWith('appeal_accept;') || + interaction.data.custom_id.startsWith('appeal_deny;') + ) { + const [action, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [ + 'appeal_accept' | 'appeal_deny', + PunishmentTypePresent, + Snowflake, + Snowflake, + string + ]; + + if (action === 'appeal_deny') { + await client.users + .send(userId, `Your ${punishment} appeal has been denied in ${client.guilds.resolve(guildId)!}.`) + .catch(() => {}); + + void respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + components: [ + { + type: 1, + components: [ + { + type: ComponentType.Button, + style: ButtonStyle.Danger, + label: 'Close', + custom_id: 'appeal_denied' + } + ] + } + ] + } + }); + } + } + } else if (interaction.type === InteractionType.ModalSubmit) { + if (interaction.data.custom_id.startsWith('appeal_submit;')) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [ + 'appeal_submit', + PunishmentTypePresent, + Snowflake, + Snowflake, + string + ]; + + const guild = client.guilds.resolve(guildId); + if (!guild) + return respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `${util.emojis.error} I am no longer in that server.` + } + }); + + const channel = await guild.getLogChannel('appeals'); + if (!channel) + return respond({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `${util.emojis.error} ${guild.name} has misconfigured their appeals channel.` + } + }); + + assert(interaction.user); + const user = new BushUser(client, interaction.user as any); + assert(user); + + const caseId = await ModLog.findOne({ where: { user: userId, guild: guildId, id: modlogCase } }); + + const embed = new Embed() + .setTitle(`${util.capitalize(punishment)} Appeal`) + .setColor(util.colors.newBlurple) + .setTimestamp() + .setFooter({ text: `CaseID: ${modlogCase}` }) + .setAuthor({ name: user.tag, iconURL: user.displayAvatarURL() }) + .addField({ + name: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`, + value: interaction.data.components![0].components[0]!.value.substring(0, 1024) + }) + .addField({ + name: 'Do you believe it was fair?', + value: interaction.data.components![1].components[0]!.value.substring(0, 1024) + }) + .addField({ + name: `Why should your ${punishment} be removed?`, + value: interaction.data.components![2].components[0]!.value.substring(0, 1024) + }) + .toJSON() as APIEmbed; + + const components = [ + new ActionRow({ + type: 1, + components: [ + // @ts-expect-error: outdated @discord.js/builders + new ButtonComponent({ + type: 2, + custom_id: `appeal_accept;${punishment};${guildId};${userId};${modlogCase}`, + label: 'Accept', + style: 3 /* Success */ + }).toJSON(), + // @ts-expect-error: outdated @discord.js/builders + new ButtonComponent({ + type: 2, + custom_id: `appeal_deny;${punishment};${guildId};${userId};${modlogCase}`, + label: 'Deny', + style: 4 /* Danger */ + }).toJSON() + ] + }) + ]; + + await channel.send({ embeds: [embed], components }); + } else { + return deferredMessageUpdate(); + } } } } diff --git a/yarn.lock b/yarn.lock index b0e784e..23c0b0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,61 +15,55 @@ __metadata: linkType: hard "@discordjs/builders@npm:^0.13.0-dev": - version: 0.13.0-dev.1644408664.fe11ff5 - resolution: "@discordjs/builders@npm:0.13.0-dev.1644408664.fe11ff5" + version: 0.13.0-dev.1644753796.3ae6f3c + resolution: "@discordjs/builders@npm:0.13.0-dev.1644753796.3ae6f3c" dependencies: "@sindresorhus/is": ^4.4.0 discord-api-types: ^0.26.1 ts-mixer: ^6.0.0 tslib: ^2.3.1 zod: ^3.11.6 - checksum: ac70f545ffd87e3bc4f794e4bd759ec9dfdffd02bc46f8d06257166345c5cd18f28dd67c98e225c8882a9417f2b8c1abea1f44aeadb4432281eb733ac1e009e4 - languageName: node - linkType: hard - -"@discordjs/collection@npm:^0.4.0": - version: 0.4.0 - resolution: "@discordjs/collection@npm:0.4.0" - checksum: fa8fc4246921f3230eb6c5d6d4dc0caf9dd659fcc903175944edf4fb0a9ed9913fdf164733d3f1e644ef469bc79b0d38a526ee620b92169cb40e79b40b0c716b + checksum: c9456389f7e0e0ccccaee0bc06924d6a92e47e941c334258b555c50c7d64cac72caafdbdbd913df62f107d21cd42d1f01f0dcbc75b9396a9ac3e3ea849cb501f languageName: node linkType: hard "@discordjs/collection@npm:^0.6.0-dev": - version: 0.6.0-dev.1644408625.fe11ff5 - resolution: "@discordjs/collection@npm:0.6.0-dev.1644408625.fe11ff5" - checksum: 8035fc07ad1d7a302c8c70f3ee18db479dc51e07e9680f764af9039c8086001665722bb222fb5ebde633ca85690aac536403e03ad5dbd5cd7ffdc86690759cee + version: 0.6.0-dev.1644753793.3ae6f3c + resolution: "@discordjs/collection@npm:0.6.0-dev.1644753793.3ae6f3c" + checksum: 2ac348f3d5a0d9b3eb23e03db255b6de52a1dbdf8047061ff67adc83d7826a1c638de2d38007e20d87d76a7f1fb1a6050deed60ae9867916d1a66dc26a1cd1f8 languageName: node linkType: hard -"@discordjs/rest@npm:^0.3.0-dev": - version: 0.3.0 - resolution: "@discordjs/rest@npm:0.3.0" +"@discordjs/rest@npm:^0.4.0-dev": + version: 0.4.0-dev.1644753853.3ae6f3c + resolution: "@discordjs/rest@npm:0.4.0-dev.1644753853.3ae6f3c" dependencies: - "@discordjs/collection": ^0.4.0 - "@sapphire/async-queue": ^1.1.9 - "@sapphire/snowflake": ^3.0.1 + "@discordjs/collection": ^0.6.0-dev + "@sapphire/async-queue": ^1.2.0 + "@sapphire/snowflake": ^3.1.0 + "@types/node-fetch": ^2.5.12 discord-api-types: ^0.26.1 form-data: ^4.0.0 - node-fetch: ^2.6.5 + node-fetch: ^2.6.7 tslib: ^2.3.1 - checksum: 0e5724156e0375b2181036d25d8847c5b7d8ab46a3409a19dad57ec9b3301d9127917a52558d3daa7e2b513804d4de9fcd5f6d56e056cc48dd567ebf26548c6d + checksum: 6871129ea94619e2f97282cad23fc3dd70580869cdbc4e76d773fc2d9e06944dc31580dc6f46bfccf4bb0b9bde844a68900c82222526b088181900eeffcf22fa languageName: node linkType: hard -"@eslint/eslintrc@npm:^1.0.5": - version: 1.0.5 - resolution: "@eslint/eslintrc@npm:1.0.5" +"@eslint/eslintrc@npm:^1.1.0": + version: 1.1.0 + resolution: "@eslint/eslintrc@npm:1.1.0" dependencies: ajv: ^6.12.4 debug: ^4.3.2 - espree: ^9.2.0 + espree: ^9.3.1 globals: ^13.9.0 ignore: ^4.0.6 import-fresh: ^3.2.1 js-yaml: ^4.1.0 minimatch: ^3.0.4 strip-json-comments: ^3.1.1 - checksum: b35b50d7b65bd8acd92a05b6fb15ac62c0cefa40dfef0324ca5bf8632bf3679bab6e173c53b3ad1e1d837701cecdbd9c144b35f46588cdf4e046a9caa272488d + checksum: 784aa2157e2808b52bbbaf1d1cfca9a6ba0b2faaa3696eb7a1229d4b357400fbd8a6aa09a16e7ae0868ea075d3a8f365cf5928b6d05a1df47f40a1167423a4fa languageName: node linkType: hard @@ -199,111 +193,111 @@ __metadata: languageName: node linkType: hard -"@sapphire/async-queue@npm:^1.1.9": +"@sapphire/async-queue@npm:^1.2.0": version: 1.2.0 resolution: "@sapphire/async-queue@npm:1.2.0" checksum: 9959c91fe031e9350134740b68e64798eff1f72f1417f312a4f7bebbd875035a406ba5ae1e71640c3819dec10d0f86a0588b494088f353f85701f2f1196e4560 languageName: node linkType: hard -"@sapphire/snowflake@npm:^3.0.1, @sapphire/snowflake@npm:^3.1.0": +"@sapphire/snowflake@npm:^3.1.0": version: 3.1.0 resolution: "@sapphire/snowflake@npm:3.1.0" checksum: 979d41f531983b992e65f79a75016e92bb4f3984148bd7e2164059b4e8e18df0206c36c5a1a02f32c39c425b268f2e7871d9eef1eb5f1690f8837e451cc00812 languageName: node linkType: hard -"@sentry/core@npm:6.17.6": - version: 6.17.6 - resolution: "@sentry/core@npm:6.17.6" +"@sentry/core@npm:6.17.7": + version: 6.17.7 + resolution: "@sentry/core@npm:6.17.7" dependencies: - "@sentry/hub": 6.17.6 - "@sentry/minimal": 6.17.6 - "@sentry/types": 6.17.6 - "@sentry/utils": 6.17.6 + "@sentry/hub": 6.17.7 + "@sentry/minimal": 6.17.7 + "@sentry/types": 6.17.7 + "@sentry/utils": 6.17.7 tslib: ^1.9.3 - checksum: 66909a0db7301553581e85fb3df2e4b1b2050dc19e24d806d4551c1b5eed14ffa95462053d8e0e3f81ad32da0871b94945abad9f44ad9d5a178ecbf4ced3b326 + checksum: c42cf0046b0c33131d31acfa73121ff0774c4e447dd47f4e989d0a083355280175ead99a5cc121813078bc06d22d9bfa19bba046b66e5bae9670b7d86cd68732 languageName: node linkType: hard -"@sentry/hub@npm:6.17.6": - version: 6.17.6 - resolution: "@sentry/hub@npm:6.17.6" +"@sentry/hub@npm:6.17.7": + version: 6.17.7 + resolution: "@sentry/hub@npm:6.17.7" dependencies: - "@sentry/types": 6.17.6 - "@sentry/utils": 6.17.6 + "@sentry/types": 6.17.7 + "@sentry/utils": 6.17.7 tslib: ^1.9.3 - checksum: 6446505332a3d2fc07e50f456dcf8782a0c457da75df30e426bbe1f3a39b309232560074b18bc2b0cd355bec4d3be5eb99f013ef9379aadead9373dd06a02f80 + checksum: 9b18cfe9f54eac2fe8e04fbfc4ed3388b875d3b486c60ec3bcd9558673dc83a070abaaea2f1810c92a863f20c629f7e38bce55a33fa7f4c30c6f63c1274add15 languageName: node linkType: hard -"@sentry/integrations@npm:^6.17.6": - version: 6.17.6 - resolution: "@sentry/integrations@npm:6.17.6" +"@sentry/integrations@npm:^6.17.7": + version: 6.17.7 + resolution: "@sentry/integrations@npm:6.17.7" dependencies: - "@sentry/types": 6.17.6 - "@sentry/utils": 6.17.6 + "@sentry/types": 6.17.7 + "@sentry/utils": 6.17.7 localforage: ^1.8.1 tslib: ^1.9.3 - checksum: 1b64ed2038b08b305a37aece683d25f7c3ff93b7508c630be50b3416252c701bcd93f4e9ae4310e57e2db442602947a9ee72eb26067e1875da35dacde61e9884 + checksum: e77b18c869cd812615806956134ee16bbcd6cc25dc5f1b939fdf2071c93fe8bd567cf4ec8fdefb5c6b6b58cfebbcd99e6cf5bde1b546b353e83a520d6e83e1a2 languageName: node linkType: hard -"@sentry/minimal@npm:6.17.6": - version: 6.17.6 - resolution: "@sentry/minimal@npm:6.17.6" +"@sentry/minimal@npm:6.17.7": + version: 6.17.7 + resolution: "@sentry/minimal@npm:6.17.7" dependencies: - "@sentry/hub": 6.17.6 - "@sentry/types": 6.17.6 + "@sentry/hub": 6.17.7 + "@sentry/types": 6.17.7 tslib: ^1.9.3 - checksum: c721f5306369569af43a0dbe4d612e8ff7b0fb228b01313060bd207276b3f2302c3227fd776db2b0920fcec8ab7ed308545c4bb2e1b5d54ee2a70addb6232f22 + checksum: 2dfbc9ee02b9e2b53e1fabdb84a1ea2cb8ce69c89b1b69e023a2c1940f1365d502be0caec1e744f46ea287f8466c9d9c190ee91f98332e15d1b35680a9b2c55a languageName: node linkType: hard -"@sentry/node@npm:^6.17.6": - version: 6.17.6 - resolution: "@sentry/node@npm:6.17.6" +"@sentry/node@npm:^6.17.7": + version: 6.17.7 + resolution: "@sentry/node@npm:6.17.7" dependencies: - "@sentry/core": 6.17.6 - "@sentry/hub": 6.17.6 - "@sentry/tracing": 6.17.6 - "@sentry/types": 6.17.6 - "@sentry/utils": 6.17.6 + "@sentry/core": 6.17.7 + "@sentry/hub": 6.17.7 + "@sentry/tracing": 6.17.7 + "@sentry/types": 6.17.7 + "@sentry/utils": 6.17.7 cookie: ^0.4.1 https-proxy-agent: ^5.0.0 lru_map: ^0.3.3 tslib: ^1.9.3 - checksum: 2d66c5797605fc1a3c76da775565dee49dcbbfb874eeeb98c1d3b624dc8861ae880204b63dba5cda274b62af4d3d0169180eeb139a4ed5a108ee3e137f656d2a + checksum: 0a80fac448f815c8e4b6e487ec226ea83bcde8972a8b83af7cbf679cd5655123d0e4e5c50c09274a569d61032156964cb1f7b59b3e406914430c0130afea61f4 languageName: node linkType: hard -"@sentry/tracing@npm:6.17.6, @sentry/tracing@npm:^6.17.6": - version: 6.17.6 - resolution: "@sentry/tracing@npm:6.17.6" +"@sentry/tracing@npm:6.17.7, @sentry/tracing@npm:^6.17.7": + version: 6.17.7 + resolution: "@sentry/tracing@npm:6.17.7" dependencies: - "@sentry/hub": 6.17.6 - "@sentry/minimal": 6.17.6 - "@sentry/types": 6.17.6 - "@sentry/utils": 6.17.6 + "@sentry/hub": 6.17.7 + "@sentry/minimal": 6.17.7 + "@sentry/types": 6.17.7 + "@sentry/utils": 6.17.7 tslib: ^1.9.3 - checksum: 2f46a6f2a79152efc2639dc50e6df1c9b29db4f9bc7eaaae5251be7b998ecb7685fe003644b7b250637cdd8a3a30ab1dfe78687661ec726a1cb8016aafb0a154 + checksum: 76f8540a331ba0180df42501deb2a4fad8ac4b5a7c469f53a0ef25d6227c765fc991bad5c28c1bfc694b9f17d6a5cf0854b3c6a4ced0f2147e2196af8a266b34 languageName: node linkType: hard -"@sentry/types@npm:6.17.6, @sentry/types@npm:^6.17.6": - version: 6.17.6 - resolution: "@sentry/types@npm:6.17.6" - checksum: c8b080202271f5cbacc91e070f6d02ce4fd87089e7ad038036b1b03b46340ed9e5edbea4ce47738107c547fd6aabd687ca376419dcb2343faad8a5f7b7de4873 +"@sentry/types@npm:6.17.7, @sentry/types@npm:^6.17.7": + version: 6.17.7 + resolution: "@sentry/types@npm:6.17.7" + checksum: 6e6bc8c46e78f921e0c85e46946349fb7347ef0af83c0cf3086ab022b15d003780acd02f4f29beb2aefdde3b9a512bd4f31d47f0bea4cee60eab60c117a733d9 languageName: node linkType: hard -"@sentry/utils@npm:6.17.6": - version: 6.17.6 - resolution: "@sentry/utils@npm:6.17.6" +"@sentry/utils@npm:6.17.7": + version: 6.17.7 + resolution: "@sentry/utils@npm:6.17.7" dependencies: - "@sentry/types": 6.17.6 + "@sentry/types": 6.17.7 tslib: ^1.9.3 - checksum: b57ce769e41b98d93ba5ed719998b93fd93a52c9dd7465b19d44c82d2a0974e72eea417e1b97ca471cd6a81474e28aaf62eee8712d5adfb2b93f10c8ba881799 + checksum: dd41419a232ae7547bbe9bd66297b197c1f6daab780e12919adcb3bb09d1c631d269ed8a65cac3a070bb8ca646006b062a5de16104fcb5a7de465238c90d04f2 languageName: node linkType: hard @@ -478,6 +472,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.5.12": + version: 2.5.12 + resolution: "@types/node-fetch@npm:2.5.12" + dependencies: + "@types/node": "*" + form-data: ^3.0.0 + checksum: ad63c85ba6a9477b8e057ec8682257738130d98e8ece4e31141789bd99df9d9147985cc8bc0cb5c8983ed5aa6bb95d46df23d1e055f4ad5cf8b82fc69cf626c7 + languageName: node + linkType: hard + "@types/node-os-utils@npm:^1.2.0": version: 1.2.0 resolution: "@types/node-os-utils@npm:1.2.0" @@ -916,10 +920,10 @@ __metadata: "@notenoughupdates/simplify-number": ^1.0.1 "@notenoughupdates/wolfram-alpha-api": ^1.0.1 "@sapphire/snowflake": ^3.1.0 - "@sentry/integrations": ^6.17.6 - "@sentry/node": ^6.17.6 - "@sentry/tracing": ^6.17.6 - "@sentry/types": ^6.17.6 + "@sentry/integrations": ^6.17.7 + "@sentry/node": ^6.17.7 + "@sentry/tracing": ^6.17.7 + "@sentry/types": ^6.17.7 "@types/eslint": ^8.4.1 "@types/express": ^4.17.13 "@types/lodash": ^4.14.178 @@ -937,13 +941,13 @@ __metadata: chalk: ^5.0.0 deep-lock: ^1.0.0 discord-akairo: "npm:@notenoughupdates/discord-akairo@dev" - discord-api-types: 0.26.1 + discord-api-types: 0.27.0 + discord-api-types-next: "npm:discord-api-types@next" discord.js: "npm:@notenoughupdates/discord.js@dev" - eslint: ^8.8.0 + eslint: ^8.9.0 eslint-config-prettier: ^8.3.0 eslint-plugin-deprecation: ^1.3.2 eslint-plugin-import: ^2.25.4 - eslint-plugin-node: ^11.1.0 fuse.js: ^6.5.3 got: ^12.0.1 lodash: ^4.17.21 @@ -955,7 +959,7 @@ __metadata: pg: ^8.7.3 pg-hstore: ^2.3.4 prettier: ^2.5.1 - pretty-bytes: ^5.6.0 + pretty-bytes: ^6.0.0 rimraf: ^3.0.2 sequelize: 6.16.1 tinycolor2: ^1.4.2 @@ -1286,13 +1290,27 @@ __metadata: linkType: hard "discord-akairo@npm:@notenoughupdates/discord-akairo@dev": - version: 9.0.10-dev.1644108723.7814d7c - resolution: "@notenoughupdates/discord-akairo@npm:9.0.10-dev.1644108723.7814d7c" - checksum: a55770b6f1dec4edd3bc2771cb896aeb12b329d6ed9b007c3fc111430665a1e84fb46d64e0cc8682d4092e9c70d3cf28cfdc77d6a171fda3d8d5cfce7edbfdf5 + version: 9.1.2-dev.1644723718.99887ac + resolution: "@notenoughupdates/discord-akairo@npm:9.1.2-dev.1644723718.99887ac" + checksum: 9a6abf342d4abadcf8ee6af3e20235399bcac215ad58d4cbacd6a6f39dbfd32e8601574a1561cb5b538e82ac05f47864cf8c4d2a3586505f47284a45efcb49c8 languageName: node linkType: hard -"discord-api-types@npm:0.26.1, discord-api-types@npm:^0.26.1": +"discord-api-types-next@npm:discord-api-types@next": + version: 0.28.0-next.ed1f717.1644755287 + resolution: "discord-api-types@npm:0.28.0-next.ed1f717.1644755287" + checksum: d33ee0264a6e3669951b9c3f157c7010802736dbd142937258dc34bf27bd71a98667c96fb65d61603429c606d5a19a9c017bdf74dfa4988ea9a2841a99b1dde0 + languageName: node + linkType: hard + +"discord-api-types@npm:0.27.0, discord-api-types@npm:^0.27.0": + version: 0.27.0 + resolution: "discord-api-types@npm:0.27.0" + checksum: 5a74a49ad7e57ea24e67d431de30cc7056d6d422b607c7d5a7dd35c683c8b87d70ec35a0d3929971adb411acc3df2bd6a77c1401ce30b29690bd1305e427265c + languageName: node + linkType: hard + +"discord-api-types@npm:^0.26.1": version: 0.26.1 resolution: "discord-api-types@npm:0.26.1" checksum: e53bfa7589b24108e6b403dbe213da34c4592f72e2b8fde6800dcb6c703065887ecbd644e1cdf694e4c7796954bc51462ced8