blob: 5425714ac8349401b15927edaaa8846eb3b3d6ab [file] [log] [blame]
PineaFana34d04b2023-01-03 22:05:42 +00001import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder } from "discord.js";
pineafan3a02ea32022-08-11 21:35:04 +01002import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
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;
PineaFan538d3752023-01-12 21:48:23 +000032 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({
96 filter: (m) => m.user.id === interaction.user.id,
97 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,
149 emoji: "PUNISH.BAN.RED",
150 timestamp: new Date().getTime()
151 },
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);
163 let out = "";
Skyler Grey75ea9172022-08-06 10:22:23 +0100164 deleted.reverse().forEach((message) => {
Skyler Grey11236ba2022-08-08 21:13:33 +0100165 out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date(
Skyler Grey75ea9172022-08-06 10:22:23 +0100166 message.createdTimestamp
167 ).toISOString()}]\n`;
pineafan63fc5e22022-08-04 22:04:10 +0100168 const lines = message.content.split("\n");
Skyler Grey75ea9172022-08-06 10:22:23 +0100169 lines.forEach((line) => {
170 out += `> ${line}\n`;
171 });
pineafan63fc5e22022-08-04 22:04:10 +0100172 out += "\n\n";
173 });
174 const attachmentObject = {
175 attachment: Buffer.from(out),
176 name: `purge-${channel.id}-${Date.now()}.txt`,
177 description: "Purge log"
178 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100179 const m = (await interaction.editReply({
180 embeds: [
181 new EmojiEmbed()
182 .setEmoji("CHANNEL.PURGE.GREEN")
183 .setTitle("Purge")
184 .setDescription("Messages cleared")
185 .setStatus("Success")
186 ],
187 components: [
PineaFana34d04b2023-01-03 22:05:42 +0000188 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -0400189 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100190 .setCustomId("download")
191 .setLabel("Download transcript")
TheCodedProf21c08592022-09-13 14:14:43 -0400192 .setStyle(ButtonStyle.Success)
Skyler Grey75ea9172022-08-06 10:22:23 +0100193 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
194 ])
195 ]
196 })) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000197 let component;
198 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100199 component = await m.awaitMessageComponent({
200 filter: (m) => m.user.id === interaction.user.id,
201 time: 300000
202 });
203 } catch {
204 return;
205 }
PineaFana34d04b2023-01-03 22:05:42 +0000206 if (component.customId === "download") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100207 interaction.editReply({
208 embeds: [
209 new EmojiEmbed()
210 .setEmoji("CHANNEL.PURGE.GREEN")
211 .setTitle("Purge")
212 .setDescription("Uploaded")
213 .setStatus("Success")
214 ],
215 components: [],
216 files: [attachmentObject]
217 });
pineafan5d1908e2022-02-28 21:34:47 +0000218 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100219 interaction.editReply({
220 embeds: [
221 new EmojiEmbed()
222 .setEmoji("CHANNEL.PURGE.GREEN")
223 .setTitle("Purge")
224 .setDescription("Messages cleared")
225 .setStatus("Success")
226 ],
227 components: []
228 });
pineafan5d1908e2022-02-28 21:34:47 +0000229 }
pineafan63fc5e22022-08-04 22:04:10 +0100230 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000231 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100232 const confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000233 .setEmoji("CHANNEL.PURGE.RED")
234 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +0100235 .setDescription(
236 keyValueList({
237 channel: `<#${channel.id}>`,
PineaFana34d04b2023-01-03 22:05:42 +0000238 amount: (interaction.options.get("amount")?.value as number).toString(),
239 reason: `\n> ${interaction.options.get("reason")?.value ? interaction.options.get("reason")?.value : "*No reason provided*"}`
Skyler Grey75ea9172022-08-06 10:22:23 +0100240 })
241 )
pineafan8b4b17f2022-02-27 20:42:52 +0000242 .setColor("Danger")
PineaFan1dee28f2023-01-16 22:09:07 +0000243 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100244 .send();
PineaFan1dee28f2023-01-16 22:09:07 +0000245 if (confirmation.cancelled || !confirmation.success) return;
246 let messages;
247 try {
248 if (!user) {
249 const toDelete = await (interaction.channel as TextChannel).messages.fetch({
250 limit: interaction.options.get("amount")?.value as number
Skyler Grey75ea9172022-08-06 10:22:23 +0100251 });
PineaFan1dee28f2023-01-16 22:09:07 +0000252 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
253 } else {
254 const toDelete = (
255 await (
256 await (interaction.channel as TextChannel).messages.fetch({
257 limit: 100
258 })
259 ).filter((m) => m.author.id === user.id)
260 ).first(interaction.options.get("amount")?.value as number);
261 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
pineafan8b4b17f2022-02-27 20:42:52 +0000262 }
PineaFan1dee28f2023-01-16 22:09:07 +0000263 } catch (e) {
264 await interaction.editReply({
265 embeds: [
266 new EmojiEmbed()
267 .setEmoji("CHANNEL.PURGE.RED")
268 .setTitle("Purge")
269 .setDescription("Something went wrong and no messages were deleted")
270 .setStatus("Danger")
271 ],
272 components: []
pineafan63fc5e22022-08-04 22:04:10 +0100273 });
PineaFan1dee28f2023-01-16 22:09:07 +0000274 }
275 if (!messages) {
276 await interaction.editReply({
277 embeds: [
278 new EmojiEmbed()
279 .setEmoji("CHANNEL.PURGE.RED")
280 .setTitle("Purge")
281 .setDescription("No messages could be deleted")
282 .setStatus("Danger")
283 ],
284 components: []
285 });
286 return;
287 }
288 if (user) {
289 await client.database.history.create(
290 "purge",
291 interaction.guild.id,
292 user.user,
293 interaction.user,
294 (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*",
295 null,
296 null,
297 messages.size.toString()
298 );
299 }
300 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
301 const data = {
302 meta: {
303 type: "channelPurge",
304 displayName: "Channel Purged",
305 calculateType: "messageDelete",
306 color: NucleusColors.red,
307 emoji: "PUNISH.BAN.RED",
308 timestamp: new Date().getTime()
309 },
310 list: {
311 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
312 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
313 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
314 messagesCleared: entry(messages.size.toString(), messages.size.toString())
315 },
316 hidden: {
317 guild: interaction.guild.id
318 }
319 };
320 log(data);
321 let out = "";
322 messages.reverse().forEach((message) => {
323 if (!message) {
324 out += "Unknown message\n\n"
325 } else {
326 const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" };
327 out += `${author.username}#${author.discriminator} (${author.id}) [${new Date(
328 message.createdTimestamp
329 ).toISOString()}]\n`;
330 if (message.content) {
331 const lines = message.content.split("\n");
332 lines.forEach((line) => {
333 out += `> ${line}\n`;
334 });
335 }
336 if (message.attachments.size > 0) {
337 message.attachments.forEach((attachment) => {
338 out += `Attachment > ${attachment.url}\n`;
339 });
340 }
341 out += "\n\n";
342 }
343 });
344 const attachmentObject = {
345 attachment: Buffer.from(out),
346 name: `purge-${channel.id}-${Date.now()}.txt`,
347 description: "Purge log"
348 };
349 const m = (await interaction.editReply({
350 embeds: [
351 new EmojiEmbed()
352 .setEmoji("CHANNEL.PURGE.GREEN")
353 .setTitle("Purge")
354 .setDescription("Messages cleared")
355 .setStatus("Success")
356 ],
357 components: [
358 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
359 new Discord.ButtonBuilder()
360 .setCustomId("download")
361 .setLabel("Download transcript")
362 .setStyle(ButtonStyle.Success)
363 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
364 ])
365 ]
366 })) as Discord.Message;
367 let component;
368 try {
369 component = await m.awaitMessageComponent({
370 filter: (m) => m.user.id === interaction.user.id,
371 time: 300000
372 });
373 } catch {
374 return;
375 }
376 if (component.customId === "download") {
377 interaction.editReply({
378 embeds: [
379 new EmojiEmbed()
380 .setEmoji("CHANNEL.PURGE.GREEN")
381 .setTitle("Purge")
382 .setDescription("Transcript uploaded above")
383 .setStatus("Success")
384 ],
385 components: [],
386 files: [attachmentObject]
387 });
388 } else {
389 interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100390 embeds: [
391 new EmojiEmbed()
392 .setEmoji("CHANNEL.PURGE.GREEN")
393 .setTitle("Purge")
394 .setDescription("Messages cleared")
395 .setStatus("Success")
396 ],
Skyler Grey75ea9172022-08-06 10:22:23 +0100397 components: []
398 });
pineafan8b4b17f2022-02-27 20:42:52 +0000399 }
400 }
pineafan63fc5e22022-08-04 22:04:10 +0100401};
pineafan4f164f32022-02-26 22:07:12 +0000402
pineafanbd02b4a2022-08-05 22:01:38 +0100403const check = (interaction: CommandInteraction) => {
PineaFana34d04b2023-01-03 22:05:42 +0000404 if (!interaction.guild) return false;
Skyler Grey75ea9172022-08-06 10:22:23 +0100405 const member = interaction.member as GuildMember;
PineaFana34d04b2023-01-03 22:05:42 +0000406 const me = interaction.guild.members.me!;
pineafanc1c18792022-08-03 21:41:36 +0100407 // Check if nucleus has the manage_messages permission
PineaFana34d04b2023-01-03 22:05:42 +0000408 if (!me.permissions.has("ManageMessages")) throw new Error("I do not have the *Manage Messages* permission");
pineafan8b4b17f2022-02-27 20:42:52 +0000409 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100410 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000411 // Check if the user has manage_messages permission
PineaFana34d04b2023-01-03 22:05:42 +0000412 if (!member.permissions.has("ManageMessages")) throw new Error("You do not have the *Manage Messages* permission");
pineafanc1c18792022-08-03 21:41:36 +0100413 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100414 return true;
415};
pineafan4f164f32022-02-26 22:07:12 +0000416
Skyler Grey75ea9172022-08-06 10:22:23 +0100417export { command, callback, check };