blob: 7728e565dceeadabe22f3e20de80cc596549b31d [file] [log] [blame]
pineafande618c42023-02-28 18:03:18 +00001import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder, Message } from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -05002import type { SlashCommandSubcommandBuilder } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00003import confirmationMessage from "../../utils/confirmationMessage.js";
pineafan4edb7762022-06-26 19:21:04 +01004import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00005import keyValueList from "../../utils/generateKeyValueList.js";
6import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +01007import client from "../../utils/client.js";
pineafan4f164f32022-02-26 22:07:12 +00008
9const command = (builder: SlashCommandSubcommandBuilder) =>
10 builder
pineafan63fc5e22022-08-04 22:04:10 +010011 .setName("purge")
12 .setDescription("Bulk deletes messages in a channel")
Skyler Grey75ea9172022-08-06 10:22:23 +010013 .addIntegerOption((option) =>
14 option
15 .setName("amount")
16 .setDescription("The amount of messages to delete")
17 .setRequired(false)
18 .setMinValue(1)
19 .setMaxValue(100)
20 )
21 .addUserOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010022 option.setName("user").setDescription("The user to purge messages from").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010023 )
24 .addStringOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010025 option.setName("reason").setDescription("The reason for the purge").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010026 );
pineafan4f164f32022-02-26 22:07:12 +000027
pineafan3a02ea32022-08-11 21:35:04 +010028const callback = async (interaction: CommandInteraction): Promise<unknown> => {
PineaFana34d04b2023-01-03 22:05:42 +000029 if (!interaction.guild) return;
30 const user = (interaction.options.getMember("user") as GuildMember | null);
Skyler Grey75ea9172022-08-06 10:22:23 +010031 const channel = interaction.channel as GuildChannel;
TheCodedProf088b1b22023-02-28 17:31:11 -050032 if (!channel.isTextBased()) {
pineafan8b4b17f2022-02-27 20:42:52 +000033 return await interaction.reply({
34 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010035 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000036 .setEmoji("CHANNEL.PURGE.RED")
37 .setTitle("Purge")
38 .setDescription("You cannot purge this channel")
39 .setStatus("Danger")
40 ],
41 components: [],
pineafan63fc5e22022-08-04 22:04:10 +010042 ephemeral: true
43 });
pineafan8b4b17f2022-02-27 20:42:52 +000044 }
45 // TODO:[Modals] Replace this with a modal
PineaFana34d04b2023-01-03 22:05:42 +000046 if (!interaction.options.get("amount")) {
pineafan8b4b17f2022-02-27 20:42:52 +000047 await interaction.reply({
48 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010049 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000050 .setEmoji("CHANNEL.PURGE.RED")
51 .setTitle("Purge")
52 .setDescription("Select how many messages to delete")
53 .setStatus("Danger")
54 ],
55 components: [],
56 ephemeral: true,
57 fetchReply: true
pineafan63fc5e22022-08-04 22:04:10 +010058 });
59 let deleted = [] as Discord.Message[];
Skyler Greyad002172022-08-16 18:48:26 +010060 let timedOut = false;
61 let amountSelected = false;
62 while (!timedOut && !amountSelected) {
Skyler Grey75ea9172022-08-06 10:22:23 +010063 const m = (await interaction.editReply({
pineafan8b4b17f2022-02-27 20:42:52 +000064 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010065 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000066 .setEmoji("CHANNEL.PURGE.RED")
67 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +010068 .setDescription(
69 "Select how many messages to delete. You can continue clicking until all messages are cleared."
70 )
pineafan8b4b17f2022-02-27 20:42:52 +000071 .setStatus("Danger")
72 ],
73 components: [
PineaFana34d04b2023-01-03 22:05:42 +000074 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040075 new Discord.ButtonBuilder().setCustomId("1").setLabel("1").setStyle(ButtonStyle.Secondary),
76 new Discord.ButtonBuilder().setCustomId("3").setLabel("3").setStyle(ButtonStyle.Secondary),
77 new Discord.ButtonBuilder().setCustomId("5").setLabel("5").setStyle(ButtonStyle.Secondary)
pineafan63fc5e22022-08-04 22:04:10 +010078 ]),
PineaFana34d04b2023-01-03 22:05:42 +000079 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040080 new Discord.ButtonBuilder().setCustomId("10").setLabel("10").setStyle(ButtonStyle.Secondary),
81 new Discord.ButtonBuilder().setCustomId("25").setLabel("25").setStyle(ButtonStyle.Secondary),
82 new Discord.ButtonBuilder().setCustomId("50").setLabel("50").setStyle(ButtonStyle.Secondary)
pineafan8b4b17f2022-02-27 20:42:52 +000083 ]),
PineaFana34d04b2023-01-03 22:05:42 +000084 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040085 new Discord.ButtonBuilder()
pineafan8b4b17f2022-02-27 20:42:52 +000086 .setCustomId("done")
87 .setLabel("Done")
TheCodedProf21c08592022-09-13 14:14:43 -040088 .setStyle(ButtonStyle.Success)
pineafan8b4b17f2022-02-27 20:42:52 +000089 .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
90 ])
91 ]
Skyler Grey75ea9172022-08-06 10:22:23 +010092 })) as Discord.Message;
pineafan8b4b17f2022-02-27 20:42:52 +000093 let component;
94 try {
Skyler Grey75ea9172022-08-06 10:22:23 +010095 component = m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -050096 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
Skyler Grey75ea9172022-08-06 10:22:23 +010097 time: 300000
98 });
99 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100100 timedOut = true;
101 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100102 }
TheCodedProf21c08592022-09-13 14:14:43 -0400103 (await component).deferUpdate();
104 if ((await component).customId === "done") {
Skyler Greyad002172022-08-16 18:48:26 +0100105 amountSelected = true;
106 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100107 }
TheCodedProf21c08592022-09-13 14:14:43 -0400108 const amount = parseInt((await component).customId);
Skyler Greyad002172022-08-16 18:48:26 +0100109
PineaFana34d04b2023-01-03 22:05:42 +0000110 let messages: Discord.Message[] = [];
Skyler Grey11236ba2022-08-08 21:13:33 +0100111 await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => {
112 if (user) {
113 ms = ms.filter((m) => m.author.id === user.id);
114 }
PineaFana34d04b2023-01-03 22:05:42 +0000115 messages = (await (channel as TextChannel).bulkDelete(ms, true)).map(m => m as Discord.Message);
Skyler Grey11236ba2022-08-08 21:13:33 +0100116 });
PineaFana34d04b2023-01-03 22:05:42 +0000117 deleted = deleted.concat(messages);
pineafan8b4b17f2022-02-27 20:42:52 +0000118 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100119 if (deleted.length === 0)
120 return await interaction.editReply({
121 embeds: [
122 new EmojiEmbed()
123 .setEmoji("CHANNEL.PURGE.RED")
124 .setTitle("Purge")
125 .setDescription("No messages were deleted")
126 .setStatus("Danger")
127 ],
128 components: []
129 });
pineafan4edb7762022-06-26 19:21:04 +0100130 if (user) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100131 await client.database.history.create(
132 "purge",
133 interaction.guild.id,
PineaFana34d04b2023-01-03 22:05:42 +0000134 user.user,
135 interaction.user,
136 (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*",
Skyler Grey75ea9172022-08-06 10:22:23 +0100137 null,
138 null,
PineaFana34d04b2023-01-03 22:05:42 +0000139 deleted.length.toString()
Skyler Grey75ea9172022-08-06 10:22:23 +0100140 );
pineafan4edb7762022-06-26 19:21:04 +0100141 }
Skyler Grey11236ba2022-08-08 21:13:33 +0100142 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +0100143 const data = {
144 meta: {
145 type: "channelPurge",
146 displayName: "Channel Purged",
147 calculateType: "messageDelete",
148 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000149 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500150 timestamp: Date.now()
pineafan63fc5e22022-08-04 22:04:10 +0100151 },
152 list: {
Skyler Grey11236ba2022-08-08 21:13:33 +0100153 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
154 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
PineaFana34d04b2023-01-03 22:05:42 +0000155 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
156 messagesCleared: entry(deleted.length.toString(), deleted.length.toString())
pineafan63fc5e22022-08-04 22:04:10 +0100157 },
158 hidden: {
159 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100160 }
pineafan63fc5e22022-08-04 22:04:10 +0100161 };
162 log(data);
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500163 const newOut = await client.database.transcripts.createTranscript(deleted, interaction, interaction.member as GuildMember);
164 const transcript = client.database.transcripts.toHumanReadable(newOut);
pineafan63fc5e22022-08-04 22:04:10 +0100165 const attachmentObject = {
PineaFan0d06edc2023-01-17 22:10:31 +0000166 attachment: Buffer.from(transcript),
pineafan63fc5e22022-08-04 22:04:10 +0100167 name: `purge-${channel.id}-${Date.now()}.txt`,
168 description: "Purge log"
169 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100170 const m = (await interaction.editReply({
171 embeds: [
172 new EmojiEmbed()
173 .setEmoji("CHANNEL.PURGE.GREEN")
174 .setTitle("Purge")
175 .setDescription("Messages cleared")
176 .setStatus("Success")
177 ],
178 components: [
PineaFana34d04b2023-01-03 22:05:42 +0000179 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -0400180 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100181 .setCustomId("download")
182 .setLabel("Download transcript")
TheCodedProf21c08592022-09-13 14:14:43 -0400183 .setStyle(ButtonStyle.Success)
Skyler Grey75ea9172022-08-06 10:22:23 +0100184 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
185 ])
186 ]
187 })) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000188 let component;
189 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100190 component = await m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -0500191 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
Skyler Grey75ea9172022-08-06 10:22:23 +0100192 time: 300000
193 });
194 } catch {
195 return;
196 }
PineaFana34d04b2023-01-03 22:05:42 +0000197 if (component.customId === "download") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100198 interaction.editReply({
199 embeds: [
200 new EmojiEmbed()
201 .setEmoji("CHANNEL.PURGE.GREEN")
202 .setTitle("Purge")
203 .setDescription("Uploaded")
204 .setStatus("Success")
205 ],
206 components: [],
207 files: [attachmentObject]
208 });
pineafan5d1908e2022-02-28 21:34:47 +0000209 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100210 interaction.editReply({
211 embeds: [
212 new EmojiEmbed()
213 .setEmoji("CHANNEL.PURGE.GREEN")
214 .setTitle("Purge")
215 .setDescription("Messages cleared")
216 .setStatus("Success")
217 ],
218 components: []
219 });
pineafan5d1908e2022-02-28 21:34:47 +0000220 }
pineafan63fc5e22022-08-04 22:04:10 +0100221 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000222 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100223 const confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000224 .setEmoji("CHANNEL.PURGE.RED")
225 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +0100226 .setDescription(
227 keyValueList({
228 channel: `<#${channel.id}>`,
PineaFana34d04b2023-01-03 22:05:42 +0000229 amount: (interaction.options.get("amount")?.value as number).toString(),
230 reason: `\n> ${interaction.options.get("reason")?.value ? interaction.options.get("reason")?.value : "*No reason provided*"}`
Skyler Grey75ea9172022-08-06 10:22:23 +0100231 })
232 )
pineafan8b4b17f2022-02-27 20:42:52 +0000233 .setColor("Danger")
PineaFan1dee28f2023-01-16 22:09:07 +0000234 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100235 .send();
PineaFan1dee28f2023-01-16 22:09:07 +0000236 if (confirmation.cancelled || !confirmation.success) return;
237 let messages;
238 try {
239 if (!user) {
240 const toDelete = await (interaction.channel as TextChannel).messages.fetch({
241 limit: interaction.options.get("amount")?.value as number
Skyler Grey75ea9172022-08-06 10:22:23 +0100242 });
PineaFan1dee28f2023-01-16 22:09:07 +0000243 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
244 } else {
245 const toDelete = (
246 await (
247 await (interaction.channel as TextChannel).messages.fetch({
248 limit: 100
249 })
250 ).filter((m) => m.author.id === user.id)
251 ).first(interaction.options.get("amount")?.value as number);
252 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
pineafan8b4b17f2022-02-27 20:42:52 +0000253 }
PineaFan1dee28f2023-01-16 22:09:07 +0000254 } catch (e) {
255 await interaction.editReply({
256 embeds: [
257 new EmojiEmbed()
258 .setEmoji("CHANNEL.PURGE.RED")
259 .setTitle("Purge")
260 .setDescription("Something went wrong and no messages were deleted")
261 .setStatus("Danger")
262 ],
263 components: []
pineafan63fc5e22022-08-04 22:04:10 +0100264 });
PineaFan1dee28f2023-01-16 22:09:07 +0000265 }
266 if (!messages) {
267 await interaction.editReply({
268 embeds: [
269 new EmojiEmbed()
270 .setEmoji("CHANNEL.PURGE.RED")
271 .setTitle("Purge")
272 .setDescription("No messages could be deleted")
273 .setStatus("Danger")
274 ],
275 components: []
276 });
277 return;
278 }
279 if (user) {
280 await client.database.history.create(
281 "purge",
282 interaction.guild.id,
283 user.user,
284 interaction.user,
285 (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*",
286 null,
287 null,
288 messages.size.toString()
289 );
290 }
291 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
292 const data = {
293 meta: {
294 type: "channelPurge",
295 displayName: "Channel Purged",
296 calculateType: "messageDelete",
297 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000298 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500299 timestamp: Date.now()
PineaFan1dee28f2023-01-16 22:09:07 +0000300 },
301 list: {
302 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
303 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
304 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
305 messagesCleared: entry(messages.size.toString(), messages.size.toString())
306 },
307 hidden: {
308 guild: interaction.guild.id
309 }
310 };
311 log(data);
pineafande618c42023-02-28 18:03:18 +0000312 const messageArray: Message[] = messages.filter(message => !(
313 message!.components.some(
314 component => component.components.some(
315 child => child.customId?.includes("transcript") ?? false
316 )
317 )
318 )).map(message => message as Message);
319 const newOut = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember);
320
TheCodedProf75c51be2023-03-03 17:18:18 -0500321 const [code, key, iv] = await client.database.transcripts.create(newOut);
pineafande618c42023-02-28 18:03:18 +0000322 await interaction.editReply({
PineaFan1dee28f2023-01-16 22:09:07 +0000323 embeds: [
324 new EmojiEmbed()
325 .setEmoji("CHANNEL.PURGE.GREEN")
326 .setTitle("Purge")
327 .setDescription("Messages cleared")
328 .setStatus("Success")
329 ],
330 components: [
331 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf75c51be2023-03-03 17:18:18 -0500332 new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`).setDisabled(!code),
PineaFan1dee28f2023-01-16 22:09:07 +0000333 ])
334 ]
pineafande618c42023-02-28 18:03:18 +0000335 });
pineafan8b4b17f2022-02-27 20:42:52 +0000336 }
pineafan63fc5e22022-08-04 22:04:10 +0100337};
pineafan4f164f32022-02-26 22:07:12 +0000338
TheCodedProff86ba092023-01-27 17:10:07 -0500339const check = (interaction: CommandInteraction, partial: boolean = false) => {
PineaFana34d04b2023-01-03 22:05:42 +0000340 if (!interaction.guild) return false;
Skyler Grey75ea9172022-08-06 10:22:23 +0100341 const member = interaction.member as GuildMember;
TheCodedProff86ba092023-01-27 17:10:07 -0500342 // Check if the user has manage_messages permission
343 if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
344 if (partial) return true;
PineaFana34d04b2023-01-03 22:05:42 +0000345 const me = interaction.guild.members.me!;
pineafanc1c18792022-08-03 21:41:36 +0100346 // Check if nucleus has the manage_messages permission
PineaFan0d06edc2023-01-17 22:10:31 +0000347 if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000348 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100349 if (member.id === interaction.guild.ownerId) return true;
pineafanc1c18792022-08-03 21:41:36 +0100350 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100351 return true;
352};
pineafan4f164f32022-02-26 22:07:12 +0000353
Skyler Grey75ea9172022-08-06 10:22:23 +0100354export { command, callback, check };
TheCodedProfa112f612023-01-28 18:06:45 -0500355export const metadata = {
356 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.",
357 premiumOnly: true,
358}