From adbb5e939cebcf4c0479d66162377957f2a845af Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Wed, 6 Jul 2022 22:10:58 +0200 Subject: feat(automod): unmute button --- src/lib/common/AutoMod.ts | 148 ++++++++++++++------- src/lib/common/util/Moderation.ts | 24 +++- .../extensions/discord.js/ExtendedGuildMember.ts | 32 ++--- src/lib/utils/BushUtils.ts | 1 + 4 files changed, 137 insertions(+), 68 deletions(-) (limited to 'src/lib') diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index 970fecd..21bcb00 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -1,4 +1,4 @@ -import { banResponse, colors, emojis, format, formatError, Moderation } from '#lib'; +import { colors, emojis, format, formatError, Moderation, unmuteResponse } from '#lib'; import assert from 'assert'; import chalk from 'chalk'; import { @@ -10,8 +10,10 @@ import { PermissionFlagsBits, type ButtonInteraction, type Message, + type Snowflake, type TextChannel } from 'discord.js'; +import UnmuteCommand from '../../commands/moderation/unmute.js'; /** * Handles auto moderation functionality. @@ -165,21 +167,14 @@ export class AutoMod { .setDescription( `**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})` ) - .addFields([ - { name: 'Message Content', value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` } - ]) + .addFields({ + name: 'Message Content', + value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` + }) .setColor(color) .setTimestamp() ], - components: [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder({ - style: ButtonStyle.Danger, - label: 'Ban User', - customId: `automod;ban;${this.message.author.id};everyone mention and scam phrase` - }) - ]) - ] + components: [this.buttons(this.message.author.id, 'everyone mention and scam phrase')] }); } } @@ -332,28 +327,33 @@ export class AutoMod { 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)}` } - ]) + .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 - ? [ - new ActionRowBuilder().addComponents([ - new ButtonBuilder({ - style: ButtonStyle.Danger, - label: 'Ban User', - customId: `automod;ban;${this.message.author.id};${highestOffence.reason}` - }) - ]) - ] - : undefined + components: highestOffence.severity >= 2 ? [this.buttons(this.message.author.id, highestOffence.reason)] : undefined }); } + private buttons(userId: Snowflake, reason: string): ActionRowBuilder { + return new ActionRowBuilder().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. @@ -364,23 +364,31 @@ export class AutoMod { content: `${emojis.error} You are missing the **Ban Members** permission.`, ephemeral: true }); - const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';'); - switch (action) { - case 'ban': { - 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); + const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';') as [ + 'ban' | 'unmute', + string, + string + ]; - const check = victim ? await Moderation.permissionCheck(moderator, victim, 'ban', true) : true; + 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); - if (check !== true) + switch (action) { + case 'ban': { + if (!interaction.guild?.members.me?.permissions.has('BanMembers')) return interaction.reply({ - content: check, + 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, @@ -389,21 +397,65 @@ export class AutoMod { }); const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId; - if (result === banResponse.SUCCESS) - return interaction.reply({ - content: `${emojis.success} Successfully banned **${victimUserFormatted}**.`, - ephemeral: true - }); - else if (result === banResponse.DM_ERROR) + + 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.warn} Banned ${victimUserFormatted} however I could not send them a dm.`, + content: `${emojis.error} Cannot find member, they may have left the server.`, ephemeral: true }); - else + + if (!interaction.guild) return interaction.reply({ - content: `${emojis.error} Could not ban **${victimUserFormatted}**: \`${result}\` .`, + 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 + }); } } } diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts index cb6b4db..fc01602 100644 --- a/src/lib/common/util/Moderation.ts +++ b/src/lib/common/util/Moderation.ts @@ -1,13 +1,16 @@ import { ActivePunishment, ActivePunishmentType, + baseMuteResponse, colors, emojis, format, Guild as GuildDB, humanizeDuration, ModLog, - type ModLogType + permissionsResponse, + type ModLogType, + type ValueOf } from '#lib'; import assert from 'assert'; import { @@ -118,6 +121,25 @@ export async function permissionCheck( 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 | 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. diff --git a/src/lib/extensions/discord.js/ExtendedGuildMember.ts b/src/lib/extensions/discord.js/ExtendedGuildMember.ts index 947f9cd..f8add83 100644 --- a/src/lib/extensions/discord.js/ExtendedGuildMember.ts +++ b/src/lib/extensions/discord.js/ExtendedGuildMember.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { BushClientEvents, formatError, Moderation, ModLogType, PunishmentTypeDM, Time } from '#lib'; +import { formatError, Moderation, ModLogType, Time, type BushClientEvents, type PunishmentTypeDM, type ValueOf } from '#lib'; import { ChannelType, - GuildChannelResolvable, GuildMember, - GuildTextBasedChannel, PermissionFlagsBits, + type GuildChannelResolvable, + type GuildTextBasedChannel, type Role } from 'discord.js'; /* eslint-enable @typescript-eslint/no-unused-vars */ @@ -358,13 +358,11 @@ export class ExtendedGuildMember extends GuildMember { */ public override async bushMute(options: BushTimedPunishmentOptions): Promise { // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return muteResponse.MISSING_PERMISSIONS; - const muteRoleID = await this.guild.getSetting('muteRole'); - if (!muteRoleID) return muteResponse.NO_MUTE_ROLE; - const muteRole = this.guild.roles.cache.get(muteRoleID); - if (!muteRole) return muteResponse.MUTE_ROLE_INVALID; - if (muteRole.position >= this.guild.members.me!.roles.highest.position || muteRole.managed) - return muteResponse.MUTE_ROLE_NOT_MANAGEABLE; + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; @@ -446,13 +444,11 @@ export class ExtendedGuildMember extends GuildMember { */ public override async bushUnmute(options: BushPunishmentOptions): Promise { // checks - if (!this.guild.members.me!.permissions.has(PermissionFlagsBits.ManageRoles)) return unmuteResponse.MISSING_PERMISSIONS; - const muteRoleID = await this.guild.getSetting('muteRole'); - if (!muteRoleID) return unmuteResponse.NO_MUTE_ROLE; - const muteRole = this.guild.roles.cache.get(muteRoleID); - if (!muteRole) return unmuteResponse.MUTE_ROLE_INVALID; - if (muteRole.position >= this.guild.members.me!.roles.highest.position || muteRole.managed) - return unmuteResponse.MUTE_ROLE_NOT_MANAGEABLE; + const checks = await Moderation.checkMutePermissions(this.guild); + if (checks !== true) return checks; + + const muteRoleID = (await this.guild.getSetting('muteRole'))!; + const muteRole = this.guild.roles.cache.get(muteRoleID)!; let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; @@ -1090,8 +1086,6 @@ export interface BushTimeoutOptions extends BushPunishmentOptions { duration: number; } -type ValueOf = T[keyof T]; - export const basePunishmentResponse = Object.freeze({ SUCCESS: 'success', MODLOG_ERROR: 'error creating modlog entry', diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts index a6463cf..059d001 100644 --- a/src/lib/utils/BushUtils.ts +++ b/src/lib/utils/BushUtils.ts @@ -32,6 +32,7 @@ import { inspect as inspectUtil, promisify } from 'util'; import * as Format from '../common/util/Format.js'; export type StripPrivate = { [K in keyof T]: T[K] extends Record ? StripPrivate : T[K] }; +export type ValueOf = T[keyof T]; /** * Capitalizes the first letter of the given text -- cgit