import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder, Message } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import keyValueList from "../../utils/generateKeyValueList.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import client from "../../utils/client.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("purge") .setDescription("Bulk deletes messages in a channel") .addIntegerOption((option) => option .setName("amount") .setDescription("The amount of messages to delete") .setRequired(false) .setMinValue(1) .setMaxValue(100) ) .addUserOption((option) => option.setName("user").setDescription("The user to purge messages from").setRequired(false) ) .addStringOption((option) => option.setName("reason").setDescription("The reason for the purge").setRequired(false) ); const callback = async (interaction: CommandInteraction): Promise => { if (!interaction.guild) return; const user = interaction.options.getMember("user") as GuildMember | null; const channel = interaction.channel as GuildChannel; if (!channel.isTextBased()) { return await interaction.reply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription("You cannot purge this channel") .setStatus("Danger") ], components: [], ephemeral: true }); } // TODO:[Modals] Replace this with a modal if (!interaction.options.get("amount")) { await interaction.reply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription("Select how many messages to delete") .setStatus("Danger") ], components: [], ephemeral: true, fetchReply: true }); let deleted = [] as Discord.Message[]; let timedOut = false; let amountSelected = false; while (!timedOut && !amountSelected) { const m = (await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription( "Select how many messages to delete. You can continue clicking until all messages are cleared." ) .setStatus("Danger") ], components: [ new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder().setCustomId("1").setLabel("1").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("3").setLabel("3").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("5").setLabel("5").setStyle(ButtonStyle.Secondary) ]), new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder().setCustomId("10").setLabel("10").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("25").setLabel("25").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("50").setLabel("50").setStyle(ButtonStyle.Secondary) ]), new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder() .setCustomId("done") .setLabel("Done") .setStyle(ButtonStyle.Success) .setEmoji(getEmojiByName("CONTROL.TICK", "id")) ]) ] })) as Discord.Message; let component; try { component = m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id, time: 300000 }); } catch (e) { timedOut = true; continue; } (await component).deferUpdate(); if ((await component).customId === "done") { amountSelected = true; continue; } const amount = parseInt((await component).customId); let messages: Discord.Message[] = []; await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => { if (user) { ms = ms.filter((m) => m.author.id === user.id); } messages = (await (channel as TextChannel).bulkDelete(ms, true)).map((m) => m as Discord.Message); }); deleted = deleted.concat(messages); } if (deleted.length === 0) return await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription("No messages were deleted") .setStatus("Danger") ], components: [] }); if (user) { await client.database.history.create( "purge", interaction.guild.id, user.user, interaction.user, (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*", null, null, deleted.length.toString() ); } const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; const data = { meta: { type: "channelPurge", displayName: "Channel Purged", calculateType: "messageDelete", color: NucleusColors.red, emoji: "CHANNEL.PURGE.RED", timestamp: Date.now() }, list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), purgedBy: entry(interaction.user.id, renderUser(interaction.user)), channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)), messagesCleared: entry(deleted.length.toString(), deleted.length.toString()) }, hidden: { guild: interaction.guild.id } }; log(data); const newOut = await client.database.transcripts.createTranscript( "purge", deleted, interaction, interaction.member as GuildMember ); const transcript = client.database.transcripts.toHumanReadable(newOut); const attachmentObject = { attachment: Buffer.from(transcript), name: `purge-${channel.id}-${Date.now()}.txt`, description: "Purge log" }; const m = (await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") .setTitle("Purge") .setDescription("Messages cleared") .setStatus("Success") ], components: [ new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder() .setCustomId("download") .setLabel("Download transcript") .setStyle(ButtonStyle.Success) .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id")) ]) ] })) as Discord.Message; let component; try { component = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id, time: 300000 }); } catch { return; } if (component.customId === "download") { interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") .setTitle("Purge") .setDescription("Uploaded") .setStatus("Success") ], components: [], files: [attachmentObject] }); } else { interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") .setTitle("Purge") .setDescription("Messages cleared") .setStatus("Success") ], components: [] }); } return; } else { const confirmation = await new confirmationMessage(interaction) .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription( keyValueList({ channel: `<#${channel.id}>`, amount: (interaction.options.get("amount")?.value as number).toString(), reason: `\n> ${ interaction.options.get("reason")?.value ? interaction.options.get("reason")?.value : "*No reason provided*" }` }) ) .setColor("Danger") .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN") .send(); if (confirmation.cancelled || !confirmation.success) return; let messages; try { if (!user) { const toDelete = await (interaction.channel as TextChannel).messages.fetch({ limit: interaction.options.get("amount")?.value as number }); messages = await (channel as TextChannel).bulkDelete(toDelete, true); } else { const toDelete = ( await ( await (interaction.channel as TextChannel).messages.fetch({ limit: 100 }) ).filter((m) => m.author.id === user.id) ).first(interaction.options.get("amount")?.value as number); messages = await (channel as TextChannel).bulkDelete(toDelete, true); } } catch (e) { await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription("Something went wrong and no messages were deleted") .setStatus("Danger") ], components: [] }); } if (!messages) { await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.RED") .setTitle("Purge") .setDescription("No messages could be deleted") .setStatus("Danger") ], components: [] }); return; } if (user) { await client.database.history.create( "purge", interaction.guild.id, user.user, interaction.user, (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*", null, null, messages.size.toString() ); } const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; const data = { meta: { type: "channelPurge", displayName: "Channel Purged", calculateType: "messageDelete", color: NucleusColors.red, emoji: "CHANNEL.PURGE.RED", timestamp: Date.now() }, list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), purgedBy: entry(interaction.user.id, renderUser(interaction.user)), channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)), messagesCleared: entry(messages.size.toString(), messages.size.toString()) }, hidden: { guild: interaction.guild.id } }; log(data); const messageArray: Message[] = messages .filter( (message) => !message!.components.some((component) => component.components.some((child) => child.customId?.includes("transcript") ?? false) ) ) .map((message) => message as Message); const newOut = await client.database.transcripts.createTranscript( "purge", messageArray, interaction, interaction.member as GuildMember ); const [code, key, iv] = await client.database.transcripts.create(newOut); await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("CHANNEL.PURGE.GREEN") .setTitle("Purge") .setDescription("Messages cleared") .setStatus("Success") ], components: [ new Discord.ActionRowBuilder().addComponents([ new ButtonBuilder() .setLabel("View") .setStyle(ButtonStyle.Link) .setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`) .setDisabled(!code) ]) ] }); } }; const check = (interaction: CommandInteraction, partial: boolean = false) => { if (!interaction.guild) return false; const member = interaction.member as GuildMember; // Check if the user has manage_messages permission if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission"; if (partial) return true; const me = interaction.guild.members.me!; // Check if nucleus has the manage_messages permission if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission"; // Allow the owner to purge if (member.id === interaction.guild.ownerId) return true; // Allow purge return true; }; export { command, callback, check }; export const metadata = { longDescription: "Deletes a specified amount of messages from a channel, optionally from a specific user. Without an amount, you can repeatedly choose a number of messages to delete.", premiumOnly: true };