aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.yarnrc.yml2
-rw-r--r--lib/automod/AutomodShared.ts4
-rw-r--r--lib/common/Appeals.ts273
-rw-r--r--lib/common/ButtonPaginator.ts6
-rw-r--r--lib/common/ConfirmationPrompt.ts12
-rw-r--r--lib/common/DeleteButton.ts8
-rw-r--r--lib/common/HighlightManager.ts3
-rw-r--r--lib/common/Moderation.ts668
-rw-r--r--lib/common/tags.ts132
-rw-r--r--lib/extensions/discord-akairo/BotCommand.ts33
-rw-r--r--lib/extensions/discord-akairo/BotCommandHandler.ts11
-rw-r--r--lib/extensions/discord-akairo/BotInhibitor.ts6
-rw-r--r--lib/extensions/discord-akairo/BotInhibitorHandler.ts7
-rw-r--r--lib/extensions/discord-akairo/BotListener.ts5
-rw-r--r--lib/extensions/discord-akairo/BotListenerHandler.ts2
-rw-r--r--lib/extensions/discord-akairo/BotTask.ts2
-rw-r--r--lib/extensions/discord-akairo/BotTaskHandler.ts2
-rw-r--r--lib/extensions/discord-akairo/SlashMessage.ts2
-rw-r--r--lib/extensions/discord-akairo/TanzaniteClient.ts48
-rw-r--r--lib/extensions/discord.js/BotClientEvents.ts2
-rw-r--r--lib/extensions/discord.js/ExtendedGuild.ts40
-rw-r--r--lib/extensions/discord.js/ExtendedGuildMember.ts48
-rw-r--r--lib/extensions/discord.js/ExtendedMessage.ts8
-rw-r--r--lib/extensions/discord.js/ExtendedUser.ts2
-rw-r--r--lib/global.ts4
-rw-r--r--lib/index.ts4
-rw-r--r--lib/models/instance/ActivePunishment.ts8
-rw-r--r--lib/models/instance/Guild.ts10
-rw-r--r--lib/models/instance/Level.ts3
-rw-r--r--lib/models/instance/ModLog.ts123
-rw-r--r--lib/types/misc.ts8
-rw-r--r--lib/utils/Arg.ts2
-rw-r--r--lib/utils/BotClientUtils.ts5
-rw-r--r--lib/utils/Constants.ts3
-rw-r--r--lib/utils/ErrorHandler.ts2
-rw-r--r--lib/utils/Utils.ts16
m---------neu-item-repo0
m---------neu-item-repo-dangerous0
-rw-r--r--package.json77
-rw-r--r--src/bot.ts28
-rw-r--r--src/commands/config/config.ts17
-rw-r--r--src/commands/config/disable.ts4
-rw-r--r--src/commands/config/log.ts10
-rw-r--r--src/commands/dev/eval.ts2
-rw-r--r--src/commands/dev/superUser.ts2
-rw-r--r--src/commands/dev/test.ts156
-rw-r--r--src/commands/fun/minesweeper.ts4
-rw-r--r--src/commands/info/guildInfo.ts111
-rw-r--r--src/commands/info/help.ts4
-rw-r--r--src/commands/info/inviteInfo.ts7
-rw-r--r--src/commands/info/ping.ts2
-rw-r--r--src/commands/info/snowflake.ts48
-rw-r--r--src/commands/info/userInfo.ts112
-rw-r--r--src/commands/leveling/level.ts8
-rw-r--r--src/commands/leveling/setLevel.ts28
-rw-r--r--src/commands/leveling/setXp.ts34
-rw-r--r--src/commands/moderation/ban.ts4
-rw-r--r--src/commands/moderation/block.ts2
-rw-r--r--src/commands/moderation/evidence.ts2
-rw-r--r--src/commands/moderation/kick.ts2
-rw-r--r--src/commands/moderation/modlog.ts60
-rw-r--r--src/commands/moderation/mute.ts2
-rw-r--r--src/commands/moderation/myLogs.ts12
-rw-r--r--src/commands/moderation/role.ts2
-rw-r--r--src/commands/moderation/slowmode.ts6
-rw-r--r--src/commands/moderation/timeout.ts8
-rw-r--r--src/commands/moderation/unblock.ts8
-rw-r--r--src/commands/moderation/unmute.ts8
-rw-r--r--src/commands/moderation/untimeout.ts8
-rw-r--r--src/commands/moderation/warn.ts2
-rw-r--r--src/commands/moulberry-bush/capes.ts6
-rw-r--r--src/commands/tickets/ticket-!.ts2
-rw-r--r--src/commands/utilities/activity.ts2
-rw-r--r--src/commands/utilities/highlight-!.ts2
-rw-r--r--src/commands/utilities/highlight-block.ts2
-rw-r--r--src/commands/utilities/highlight-matches.ts2
-rw-r--r--src/commands/utilities/highlight-unblock.ts2
-rw-r--r--src/commands/utilities/price.ts4
-rw-r--r--src/commands/utilities/steal.ts8
-rw-r--r--src/commands/utilities/whoHasRole.ts2
-rw-r--r--src/commands/utilities/wolframAlpha.ts6
-rw-r--r--src/context-menu-commands/message/viewRaw.ts2
-rw-r--r--src/context-menu-commands/user/modlog.ts9
-rw-r--r--src/context-menu-commands/user/userInfo.ts28
-rw-r--r--src/listeners/bush/appealListener.ts14
-rw-r--r--src/listeners/bush/experimentYoink.ts28
-rw-r--r--src/listeners/client/ready.ts28
-rw-r--r--src/listeners/commands/commandBlocked.ts4
-rw-r--r--src/listeners/contextCommands/contextCommandBlocked.ts4
-rw-r--r--src/listeners/contextCommands/contextCommandError.ts2
-rw-r--r--src/listeners/contextCommands/contextCommandNotFound.ts2
-rw-r--r--src/listeners/contextCommands/contextCommandStarted.ts2
-rw-r--r--src/listeners/guild/syncUnbanPunishmentModel.ts2
-rw-r--r--src/listeners/interaction/$interactionCreate.ts31
-rw-r--r--src/listeners/interaction/button.ts129
-rw-r--r--src/listeners/interaction/interactionCreate.ts78
-rw-r--r--src/listeners/interaction/modalSubmit.ts20
-rw-r--r--src/listeners/interaction/selectMenu.ts23
-rw-r--r--src/listeners/member-custom/levelUpdate.ts3
-rw-r--r--src/listeners/message/autoPublisher.ts5
-rw-r--r--src/listeners/message/level.ts57
-rw-r--r--src/listeners/message/quoteCreate.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncBan.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncKick.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncTimeout.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncUnban.ts2
-rw-r--r--src/listeners/ws/INTERACTION_CREATE.ts236
-rw-r--r--src/tasks/feature/removeExpiredPunishements.ts8
-rw-r--r--yarn.lock1582
109 files changed, 2408 insertions, 2231 deletions
diff --git a/.yarnrc.yml b/.yarnrc.yml
index bd5ccfd..33ad4a3 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -2,7 +2,7 @@ enableGlobalCache: true
enableTelemetry: false
-nodeLinker: pnpm
+nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts
index 29b0536..48217dd 100644
--- a/lib/automod/AutomodShared.ts
+++ b/lib/automod/AutomodShared.ts
@@ -162,7 +162,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
ephemeral: true
});
- const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true;
+ const check = victim ? await Moderation.permissionCheck(moderator, victim, Moderation.Action.Ban, true) : true;
if (check !== true) return interaction.reply({ content: check, ephemeral: true });
const result = await interaction.guild?.customBan({
@@ -203,7 +203,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) {
ephemeral: true
});
- const check = await Moderation.permissionCheck(moderator, victim, 'unmute', true);
+ const check = await Moderation.permissionCheck(moderator, victim, Moderation.Action.Unmute, true);
if (check !== true) return interaction.reply({ content: check, ephemeral: true });
const check2 = await Moderation.checkMutePermissions(interaction.guild);
diff --git a/lib/common/Appeals.ts b/lib/common/Appeals.ts
new file mode 100644
index 0000000..43c56fd
--- /dev/null
+++ b/lib/common/Appeals.ts
@@ -0,0 +1,273 @@
+import { AppealStatus, ModLog } from '#lib/models/instance/ModLog.js';
+import { colors, emojis } from '#lib/utils/Constants.js';
+import { input } from '#lib/utils/Format.js';
+import { capitalize, ModalInput } from '#lib/utils/Utils.js';
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ EmbedBuilder,
+ TextInputStyle,
+ type ButtonInteraction,
+ type ModalSubmitInteraction,
+ type Snowflake
+} from 'discord.js';
+import assert from 'node:assert/strict';
+import { Action, punishments } from './Moderation.js';
+
+type AppealBase = 'appeal_attempt' | 'appeal_submit' | 'appeal_accept' | 'appeal_deny';
+
+type RawAppealInfo = [baseId: AppealBase, punishment: `${Action}`, guildId: Snowflake, userId: Snowflake, modlogId: string];
+
+type AppealInfo = [baseId: AppealBase, punishment: Action, guildId: Snowflake, userId: Snowflake, modlogId: string];
+
+export type AppealIdString =
+ `${RawAppealInfo[0]};${RawAppealInfo[1]};${RawAppealInfo[2]};${RawAppealInfo[3]};${RawAppealInfo[4]}`;
+
+function parseAppeal(customId: AppealIdString | string): AppealInfo {
+ const [baseId, _punishment, guildId, userId, modlogId] = customId.split(';') as RawAppealInfo;
+
+ const punishment = Action[Action[_punishment] as keyof typeof Action];
+
+ return [baseId, punishment, guildId, userId, modlogId];
+}
+
+/**
+ * Handles when a user clicks the "Appeal Punishment" button on a punishment dm.
+ * @param interaction A button interaction with a custom id thar starts with "appeal_attempt;".
+ */
+export async function handleAppealAttempt(interaction: ButtonInteraction) {
+ const [baseId, punishment, guildId, userId, modlogId] = parseAppeal(interaction.customId);
+
+ const { base, past, appealCustom } = punishments[punishment];
+ const appealName = appealCustom ?? capitalize(base);
+
+ const guild = interaction.client.guilds.resolve(guildId);
+ if (!guild) {
+ return await interaction.reply(`${emojis.error} I am no longer in that server.`);
+ }
+
+ const modlog = await ModLog.findByPk(modlogId);
+ if (!modlog) {
+ return await interaction.reply(`:skull: I cannot find the modlog ${input(modlogId)}. Please report this to my developers.`);
+ }
+
+ switch (modlog.appeal) {
+ case AppealStatus.Accepted:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(modlogId)}) has already been appealed and accepted.`
+ );
+ case AppealStatus.Denied:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(modlogId)}) has already been appealed and denied.`
+ );
+ case AppealStatus.Submitted:
+ return await interaction.reply(
+ `${emojis.error} Your punishment (${input(
+ modlogId
+ )}) has already been appealed, please be patient for a moderator to review your appeal.`
+ );
+ default: {
+ const _exhaustiveCheck: AppealStatus.None = modlog.appeal;
+ }
+ }
+
+ const baseInput = {
+ style: TextInputStyle.Paragraph,
+ required: true,
+ maxLength: 1024
+ };
+
+ return await interaction.showModal({
+ customId: `appeal_submit;${punishment};${guildId};${userId};${modlogId}`,
+ title: `${appealName} Appeal`,
+ components: [
+ ModalInput({
+ ...baseInput,
+ label: `Why were you ${past}?`,
+ placeholder: `Why do you think you received a ${base}?`,
+ customId: 'appeal_reason'
+ }),
+ ModalInput({
+ ...baseInput,
+ label: 'Do you believe it was fair?',
+ placeholder: `Do you think that your ${base} is fair?`,
+ customId: 'appeal_fair'
+ }),
+ ModalInput({
+ ...baseInput,
+ label: `Why should your ${base} be removed?`,
+ placeholder: `Why do you think your ${base} be removed?`,
+ customId: 'appeal_why'
+ })
+ ]
+ });
+}
+
+/**
+ * Handles when a user submits the modal for appealing a punishment.
+ * @param interaction A modal interaction with a custom id that starts with "appeal_submit;".
+ */
+export async function handleAppealSubmit(interaction: ModalSubmitInteraction) {
+ const [baseId, punishment, guildId, userId, modlogId] = parseAppeal(interaction.customId);
+
+ const { base, past, appealCustom } = punishments[punishment];
+ const appealName = appealCustom ?? capitalize(base);
+
+ const guild = interaction.client.guilds.resolve(guildId);
+ if (!guild) {
+ return await interaction.reply(`${emojis.error} I am no longer in that server.`);
+ }
+
+ const modlog = await ModLog.findByPk(modlogId);
+ if (!modlog) {
+ return await interaction.reply(`:skull: I cannot find the modlog ${input(modlogId)}. Please report this to my developers.`);
+ }
+
+ if (modlog.appeal !== AppealStatus.None) {
+ return await interaction.reply(`Invalid appeal status: ${modlog.appeal}`);
+ }
+
+ modlog.appeal = AppealStatus.Submitted;
+ await modlog.save();
+
+ const appealChannel = await guild.getLogChannel('appeals');
+ if (!appealChannel) {
+ return await interaction.reply(`${emojis.error} I could not find an appeals channel in this server.`);
+ }
+
+ const user = await interaction.client.users.fetch(userId);
+
+ const reason = interaction.fields.getTextInputValue('appeal_reason');
+ const fair = interaction.fields.getTextInputValue('appeal_fair');
+ const why = interaction.fields.getTextInputValue('appeal_why');
+
+ const embed = new EmbedBuilder()
+ .setTitle(`${appealName} Appeal`)
+ .setColor(colors.newBlurple)
+ .setTimestamp()
+ .setFooter({ text: `CaseID: ${modlogId}` })
+ .setAuthor({ name: user.tag, iconURL: user.displayAvatarURL() })
+ .addFields(
+ { name: `Why were you ${past}?`, value: reason },
+ { name: 'Do you believe it was fair?', value: fair },
+ { name: `Why should your ${base} be removed?`, value: why }
+ );
+ return await appealChannel.send({
+ content: `Appeal submitted by ${user.tag} (${user.id})`,
+ embeds: [embed],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder({
+ customId: `appeal_accept;${punishment};${guildId};${userId};${modlogId}`,
+ label: 'Accept Appeal',
+ style: ButtonStyle.Success
+ }),
+ new ButtonBuilder({
+ customId: `appeal_deny;${punishment};${guildId};${userId};${modlogId}`,
+ label: 'Deny Appeal',
+ style: ButtonStyle.Danger
+ })
+ )
+ ]
+ });
+}
+
+/**
+ * Handles interactions when a moderator clicks the "Accept" or "Deny" button on a punishment appeal.
+ * @param interaction A button interaction with a custom id that