From a1cec1024499be346017f851bd1c2efd07e6a945 Mon Sep 17 00:00:00 2001 From: Ali Hammoud Date: Sun, 23 Nov 2025 14:08:39 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=9F=20feat:=20add=20DM=20notification?= =?UTF-8?q?=20for=20repel=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/moderation/repel.ts | 71 ++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/commands/moderation/repel.ts b/src/commands/moderation/repel.ts index bc8f0e5..1050f0a 100644 --- a/src/commands/moderation/repel.ts +++ b/src/commands/moderation/repel.ts @@ -3,6 +3,8 @@ import { ApplicationCommandType, ChannelType, type ChatInputCommandInteraction, + Colors, + ContainerBuilder, EmbedBuilder, GuildMember, type Message, @@ -10,6 +12,7 @@ import { PermissionFlagsBits, type Role, type TextChannel, + TextDisplayBuilder, type User, } from 'discord.js'; import { HOUR, MINUTE, timeToString } from '../../constants/time.js'; @@ -90,6 +93,39 @@ const checkCanRepelTarget = ({ return { ok: true }; }; +const sendReasonToTarget = async ({ + target, + reason, + guildName, + timeoutDuration, +}: { + target: GuildMember | User; + reason: string; + guildName: string; + timeoutDuration?: number; +}): Promise => { + const parts: string[] = []; + parts.push(`You have been repelled from **${guildName}**.`); + if (timeoutDuration && timeoutDuration > 0) { + parts.push(`You have been timed out for ${timeToString(timeoutDuration)}.`); + } + parts.push(`**Reason:** ${reason}`); + const messageContent = parts.join('\n'); + + const containerComponent = new ContainerBuilder() + .setAccentColor(Colors.DarkRed) + .addTextDisplayComponents(new TextDisplayBuilder().setContent(messageContent)); + try { + await target.send({ + flags: MessageFlags.IsComponentsV2, + components: [containerComponent], + }); + return true; + } catch { + return false; + } +}; + const getTargetFromInteraction = async ( interaction: ChatInputCommandInteraction ): Promise => { @@ -215,6 +251,7 @@ const logRepelAction = async ({ deleteCount, reason, failedChannels, + dm, }: { interaction: ChatInputCommandInteraction; member: GuildMember; @@ -223,6 +260,10 @@ const logRepelAction = async ({ duration?: number; deleteCount: number; failedChannels: string[]; + dm: { + sent: boolean; + shouldSend: boolean; + }; }) => { const channelInfo = interaction.channel?.type === ChannelType.GuildVoice @@ -276,6 +317,14 @@ const logRepelAction = async ({ .setColor('Orange') .setTimestamp(); + if (dm.shouldSend) { + resultEmbed.addFields({ + name: 'DM Sent', + value: dm.sent ? 'Yes' : 'No', + inline: true, + }); + } + const failedChannelsEmbed = failedChannels.length > 0 ? new EmbedBuilder() @@ -312,6 +361,7 @@ const RepelOptions = { LOOK_BACK: 'look_back', TIMEOUT_DURATION: 'timeout_duration', MESSAGE_FOR_MODS: 'message_for_mods', + DM_USER: 'dm_user', } as const; export const repelCommand = createCommand({ @@ -372,6 +422,12 @@ export const repelCommand = createCommand({ type: ApplicationCommandOptionType.String, description: 'Optional message to include for moderators in the log', }, + { + name: RepelOptions.DM_USER, + required: false, + type: ApplicationCommandOptionType.Boolean, + description: 'Whether to DM the user about the repel action (Defaults to true)', + }, ], }, execute: async (interaction) => { @@ -442,6 +498,17 @@ export const repelCommand = createCommand({ lookBack: lookBack ?? DEFAULT_LOOK_BACK_MS, }); + const shouldDMUser = interaction.options.getBoolean(RepelOptions.DM_USER) ?? true; + let dmSent = false; + if (shouldDMUser) { + dmSent = await sendReasonToTarget({ + target, + reason, + guildName: interaction.guild.name, + timeoutDuration: timeout, + }); + } + logRepelAction({ interaction, member: commandUser, @@ -450,6 +517,10 @@ export const repelCommand = createCommand({ duration: timeout, deleteCount: deleted, failedChannels, + dm: { + sent: dmSent, + shouldSend: shouldDMUser, + }, }); await interaction.editReply({