blob: cabcb1e83b4d497ab611cc07b91f8e8e28e903c6 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from '../../utils/logTranscripts.js';
PineaFana34d04b2023-01-03 22:05:42 +00002import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder } from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -05003import type { SlashCommandSubcommandBuilder } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +00004import confirmationMessage from "../../utils/confirmationMessage.js";
pineafan4edb7762022-06-26 19:21:04 +01005import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00006import keyValueList from "../../utils/generateKeyValueList.js";
7import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +01008import client from "../../utils/client.js";
pineafan4f164f32022-02-26 22:07:12 +00009
10const command = (builder: SlashCommandSubcommandBuilder) =>
11 builder
pineafan63fc5e22022-08-04 22:04:10 +010012 .setName("purge")
13 .setDescription("Bulk deletes messages in a channel")
Skyler Grey75ea9172022-08-06 10:22:23 +010014 .addIntegerOption((option) =>
15 option
16 .setName("amount")
17 .setDescription("The amount of messages to delete")
18 .setRequired(false)
19 .setMinValue(1)
20 .setMaxValue(100)
21 )
22 .addUserOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010023 option.setName("user").setDescription("The user to purge messages from").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010024 )
25 .addStringOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010026 option.setName("reason").setDescription("The reason for the purge").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010027 );
pineafan4f164f32022-02-26 22:07:12 +000028
pineafan3a02ea32022-08-11 21:35:04 +010029const callback = async (interaction: CommandInteraction): Promise<unknown> => {
PineaFana34d04b2023-01-03 22:05:42 +000030 if (!interaction.guild) return;
31 const user = (interaction.options.getMember("user") as GuildMember | null);
Skyler Grey75ea9172022-08-06 10:22:23 +010032 const channel = interaction.channel as GuildChannel;
PineaFan538d3752023-01-12 21:48:23 +000033 if (channel.isTextBased()) {
pineafan8b4b17f2022-02-27 20:42:52 +000034 return await interaction.reply({
35 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010036 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000037 .setEmoji("CHANNEL.PURGE.RED")
38 .setTitle("Purge")
39 .setDescription("You cannot purge this channel")
40 .setStatus("Danger")
41 ],
42 components: [],
pineafan63fc5e22022-08-04 22:04:10 +010043 ephemeral: true
44 });
pineafan8b4b17f2022-02-27 20:42:52 +000045 }
46 // TODO:[Modals] Replace this with a modal
PineaFana34d04b2023-01-03 22:05:42 +000047 if (!interaction.options.get("amount")) {
pineafan8b4b17f2022-02-27 20:42:52 +000048 await interaction.reply({
49 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010050 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000051 .setEmoji("CHANNEL.PURGE.RED")
52 .setTitle("Purge")
53 .setDescription("Select how many messages to delete")
54 .setStatus("Danger")
55 ],
56 components: [],
57 ephemeral: true,
58 fetchReply: true
pineafan63fc5e22022-08-04 22:04:10 +010059 });
60 let deleted = [] as Discord.Message[];
Skyler Greyad002172022-08-16 18:48:26 +010061 let timedOut = false;
62 let amountSelected = false;
63 while (!timedOut && !amountSelected) {
Skyler Grey75ea9172022-08-06 10:22:23 +010064 const m = (await interaction.editReply({
pineafan8b4b17f2022-02-27 20:42:52 +000065 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010066 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000067 .setEmoji("CHANNEL.PURGE.RED")
68 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +010069 .setDescription(
70 "Select how many messages to delete. You can continue clicking until all messages are cleared."
71 )
pineafan8b4b17f2022-02-27 20:42:52 +000072 .setStatus("Danger")
73 ],
74 components: [
PineaFana34d04b2023-01-03 22:05:42 +000075 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040076 new Discord.ButtonBuilder().setCustomId("1").setLabel("1").setStyle(ButtonStyle.Secondary),
77 new Discord.ButtonBuilder().setCustomId("3").setLabel("3").setStyle(ButtonStyle.Secondary),
78 new Discord.ButtonBuilder().setCustomId("5").setLabel("5").setStyle(ButtonStyle.Secondary)
pineafan63fc5e22022-08-04 22:04:10 +010079 ]),
PineaFana34d04b2023-01-03 22:05:42 +000080 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040081 new Discord.ButtonBuilder().setCustomId("10").setLabel("10").setStyle(ButtonStyle.Secondary),
82 new Discord.ButtonBuilder().setCustomId("25").setLabel("25").setStyle(ButtonStyle.Secondary),
83 new Discord.ButtonBuilder().setCustomId("50").setLabel("50").setStyle(ButtonStyle.Secondary)
pineafan8b4b17f2022-02-27 20:42:52 +000084 ]),
PineaFana34d04b2023-01-03 22:05:42 +000085 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040086 new Discord.ButtonBuilder()
pineafan8b4b17f2022-02-27 20:42:52 +000087 .setCustomId("done")
88 .setLabel("Done")
TheCodedProf21c08592022-09-13 14:14:43 -040089 .setStyle(ButtonStyle.Success)
pineafan8b4b17f2022-02-27 20:42:52 +000090 .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
91 ])
92 ]
Skyler Grey75ea9172022-08-06 10:22:23 +010093 })) as Discord.Message;
pineafan8b4b17f2022-02-27 20:42:52 +000094 let component;
95 try {
Skyler Grey75ea9172022-08-06 10:22:23 +010096 component = m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -050097 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 +010098 time: 300000
99 });
100 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100101 timedOut = true;
102 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100103 }
TheCodedProf21c08592022-09-13 14:14:43 -0400104 (await component).deferUpdate();
105 if ((await component).customId === "done") {
Skyler Greyad002172022-08-16 18:48:26 +0100106 amountSelected = true;
107 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100108 }
TheCodedProf21c08592022-09-13 14:14:43 -0400109 const amount = parseInt((await component).customId);
Skyler Greyad002172022-08-16 18:48:26 +0100110
PineaFana34d04b2023-01-03 22:05:42 +0000111 let messages: Discord.Message[] = [];
Skyler Grey11236ba2022-08-08 21:13:33 +0100112 await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => {
113 if (user) {
114 ms = ms.filter((m) => m.author.id === user.id);
115 }
PineaFana34d04b2023-01-03 22:05:42 +0000116 messages = (await (channel as TextChannel).bulkDelete(ms, true)).map(m => m as Discord.Message);
Skyler Grey11236ba2022-08-08 21:13:33 +0100117 });
PineaFana34d04b2023-01-03 22:05:42 +0000118 deleted = deleted.concat(messages);
pineafan8b4b17f2022-02-27 20:42:52 +0000119 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100120 if (deleted.length === 0)
121 return await interaction.editReply({
122 embeds: [
123 new EmojiEmbed()
124 .setEmoji("CHANNEL.PURGE.RED")
125 .setTitle("Purge")
126 .setDescription("No messages were deleted")
127 .setStatus("Danger")
128 ],
129 components: []
130 });
pineafan4edb7762022-06-26 19:21:04 +0100131 if (user) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100132 await client.database.history.create(
133 "purge",
134 interaction.guild.id,
PineaFana34d04b2023-01-03 22:05:42 +0000135 user.user,
136 interaction.user,
137 (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*",
Skyler Grey75ea9172022-08-06 10:22:23 +0100138 null,
139 null,
PineaFana34d04b2023-01-03 22:05:42 +0000140 deleted.length.toString()
Skyler Grey75ea9172022-08-06 10:22:23 +0100141 );
pineafan4edb7762022-06-26 19:21:04 +0100142 }
Skyler Grey11236ba2022-08-08 21:13:33 +0100143 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +0100144 const data = {
145 meta: {
146 type: "channelPurge",
147 displayName: "Channel Purged",
148 calculateType: "messageDelete",
149 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000150 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500151 timestamp: Date.now()
pineafan63fc5e22022-08-04 22:04:10 +0100152 },
153 list: {
Skyler Grey11236ba2022-08-08 21:13:33 +0100154 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
155 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
PineaFana34d04b2023-01-03 22:05:42 +0000156 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
157 messagesCleared: entry(deleted.length.toString(), deleted.length.toString())
pineafan63fc5e22022-08-04 22:04:10 +0100158 },
159 hidden: {
160 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100161 }
pineafan63fc5e22022-08-04 22:04:10 +0100162 };
163 log(data);
PineaFan0d06edc2023-01-17 22:10:31 +0000164 const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted)!);
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);
312 let out = "";
313 messages.reverse().forEach((message) => {
314 if (!message) {
315 out += "Unknown message\n\n"
316 } else {
317 const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" };
318 out += `${author.username}#${author.discriminator} (${author.id}) [${new Date(
319 message.createdTimestamp
320 ).toISOString()}]\n`;
321 if (message.content) {
322 const lines = message.content.split("\n");
323 lines.forEach((line) => {
324 out += `> ${line}\n`;
325 });
326 }
327 if (message.attachments.size > 0) {
328 message.attachments.forEach((attachment) => {
329 out += `Attachment > ${attachment.url}\n`;
330 });
331 }
332 out += "\n\n";
333 }
334 });
335 const attachmentObject = {
336 attachment: Buffer.from(out),
337 name: `purge-${channel.id}-${Date.now()}.txt`,
338 description: "Purge log"
339 };
340 const m = (await interaction.editReply({
341 embeds: [
342 new EmojiEmbed()
343 .setEmoji("CHANNEL.PURGE.GREEN")
344 .setTitle("Purge")
345 .setDescription("Messages cleared")
346 .setStatus("Success")
347 ],
348 components: [
349 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
350 new Discord.ButtonBuilder()
351 .setCustomId("download")
352 .setLabel("Download transcript")
353 .setStyle(ButtonStyle.Success)
354 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
355 ])
356 ]
357 })) as Discord.Message;
358 let component;
359 try {
360 component = await m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -0500361 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
PineaFan1dee28f2023-01-16 22:09:07 +0000362 time: 300000
363 });
364 } catch {
365 return;
366 }
367 if (component.customId === "download") {
368 interaction.editReply({
369 embeds: [
370 new EmojiEmbed()
371 .setEmoji("CHANNEL.PURGE.GREEN")
372 .setTitle("Purge")
373 .setDescription("Transcript uploaded above")
374 .setStatus("Success")
375 ],
376 components: [],
377 files: [attachmentObject]
378 });
379 } else {
380 interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100381 embeds: [
382 new EmojiEmbed()
383 .setEmoji("CHANNEL.PURGE.GREEN")
384 .setTitle("Purge")
385 .setDescription("Messages cleared")
386 .setStatus("Success")
387 ],
Skyler Grey75ea9172022-08-06 10:22:23 +0100388 components: []
389 });
pineafan8b4b17f2022-02-27 20:42:52 +0000390 }
391 }
pineafan63fc5e22022-08-04 22:04:10 +0100392};
pineafan4f164f32022-02-26 22:07:12 +0000393
TheCodedProff86ba092023-01-27 17:10:07 -0500394const check = (interaction: CommandInteraction, partial: boolean = false) => {
PineaFana34d04b2023-01-03 22:05:42 +0000395 if (!interaction.guild) return false;
Skyler Grey75ea9172022-08-06 10:22:23 +0100396 const member = interaction.member as GuildMember;
TheCodedProff86ba092023-01-27 17:10:07 -0500397 // Check if the user has manage_messages permission
398 if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
399 if (partial) return true;
PineaFana34d04b2023-01-03 22:05:42 +0000400 const me = interaction.guild.members.me!;
pineafanc1c18792022-08-03 21:41:36 +0100401 // Check if nucleus has the manage_messages permission
PineaFan0d06edc2023-01-17 22:10:31 +0000402 if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000403 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100404 if (member.id === interaction.guild.ownerId) return true;
pineafanc1c18792022-08-03 21:41:36 +0100405 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100406 return true;
407};
pineafan4f164f32022-02-26 22:07:12 +0000408
Skyler Grey75ea9172022-08-06 10:22:23 +0100409export { command, callback, check };
TheCodedProfa112f612023-01-28 18:06:45 -0500410export const metadata = {
411 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.",
412 premiumOnly: true,
413}