blob: c554ee725936cacc4c2df5f81e069dc25ad62d7a [file] [log] [blame]
pineafan8b4b17f2022-02-27 20:42:52 +00001import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel } from "discord.js";
pineafan4f164f32022-02-26 22:07:12 +00002import { 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")
13 .addIntegerOption(option => option
14 .setName("amount")
15 .setDescription("The amount of messages to delete")
16 .setRequired(false)
17 .setMinValue(1)
18 .setMaxValue(100))
19 .addUserOption(option => option.setName("user").setDescription("The user to purge messages from").setRequired(false))
20 .addStringOption(option => option.setName("reason").setDescription("The reason for the purge").setRequired(false));
pineafan4f164f32022-02-26 22:07:12 +000021
pineafanbd02b4a2022-08-05 22:01:38 +010022const callback = async (interaction: CommandInteraction): Promise<void | unknown> => {
pineafan63fc5e22022-08-04 22:04:10 +010023 const user = interaction.options.getMember("user") as GuildMember ?? null;
24 const channel = (interaction.channel as GuildChannel);
pineafan8b4b17f2022-02-27 20:42:52 +000025 if (!(["GUILD_TEXT", "GUILD_NEWS", "GUILD_NEWS_THREAD", "GUILD_PUBLIC_THREAD", "GUILD_PRIVATE_THREAD"].includes(channel.type.toString()))) {
26 return await interaction.reply({
27 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010028 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000029 .setEmoji("CHANNEL.PURGE.RED")
30 .setTitle("Purge")
31 .setDescription("You cannot purge this channel")
32 .setStatus("Danger")
33 ],
34 components: [],
pineafan63fc5e22022-08-04 22:04:10 +010035 ephemeral: true
36 });
pineafan8b4b17f2022-02-27 20:42:52 +000037 }
38 // TODO:[Modals] Replace this with a modal
39 if ( !interaction.options.getInteger("amount") ) {
40 await interaction.reply({
41 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010042 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000043 .setEmoji("CHANNEL.PURGE.RED")
44 .setTitle("Purge")
45 .setDescription("Select how many messages to delete")
46 .setStatus("Danger")
47 ],
48 components: [],
49 ephemeral: true,
50 fetchReply: true
pineafan63fc5e22022-08-04 22:04:10 +010051 });
52 let deleted = [] as Discord.Message[];
pineafan8b4b17f2022-02-27 20:42:52 +000053 while (true) {
pineafan63fc5e22022-08-04 22:04:10 +010054 const m = await interaction.editReply({
pineafan8b4b17f2022-02-27 20:42:52 +000055 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010056 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000057 .setEmoji("CHANNEL.PURGE.RED")
58 .setTitle("Purge")
59 .setDescription("Select how many messages to delete. You can continue clicking until all messages are cleared.")
60 .setStatus("Danger")
61 ],
62 components: [
63 new Discord.MessageActionRow().addComponents([
64 new Discord.MessageButton()
65 .setCustomId("1")
66 .setLabel("1")
67 .setStyle("SECONDARY"),
68 new Discord.MessageButton()
69 .setCustomId("3")
70 .setLabel("3")
71 .setStyle("SECONDARY"),
72 new Discord.MessageButton()
73 .setCustomId("5")
74 .setLabel("5")
75 .setStyle("SECONDARY")
pineafan63fc5e22022-08-04 22:04:10 +010076 ]),
77 new Discord.MessageActionRow().addComponents([
pineafan8b4b17f2022-02-27 20:42:52 +000078 new Discord.MessageButton()
79 .setCustomId("10")
80 .setLabel("10")
81 .setStyle("SECONDARY"),
82 new Discord.MessageButton()
83 .setCustomId("25")
84 .setLabel("25")
85 .setStyle("SECONDARY"),
86 new Discord.MessageButton()
87 .setCustomId("50")
88 .setLabel("50")
89 .setStyle("SECONDARY")
90 ]),
91 new Discord.MessageActionRow().addComponents([
92 new Discord.MessageButton()
93 .setCustomId("done")
94 .setLabel("Done")
95 .setStyle("SUCCESS")
96 .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
97 ])
98 ]
pineafanbd02b4a2022-08-05 22:01:38 +010099 }) as Discord.Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000100 let component;
101 try {
pineafanbd02b4a2022-08-05 22:01:38 +0100102 component = m.awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan8b4b17f2022-02-27 20:42:52 +0000103 } catch (e) { break; }
104 component.deferUpdate();
105 if (component.customId === "done") break;
106 let amount;
107 try { amount = parseInt(component.customId); } catch { break; }
pineafan5d1908e2022-02-28 21:34:47 +0000108 let messages;
pineafane625d782022-05-09 18:04:32 +0100109 await (interaction.channel as TextChannel).messages.fetch({limit: amount}).then(async (ms) => {
pineafan5d1908e2022-02-28 21:34:47 +0000110 if (user) {
pineafan63fc5e22022-08-04 22:04:10 +0100111 ms = ms.filter(m => m.author.id === user.id);
pineafan5d1908e2022-02-28 21:34:47 +0000112 }
113 messages = await (channel as TextChannel).bulkDelete(ms, true);
pineafan63fc5e22022-08-04 22:04:10 +0100114 });
pineafane625d782022-05-09 18:04:32 +0100115 if (messages) {
pineafan63fc5e22022-08-04 22:04:10 +0100116 deleted = deleted.concat(messages.map(m => m));
pineafane625d782022-05-09 18:04:32 +0100117 }
pineafan8b4b17f2022-02-27 20:42:52 +0000118 }
119 if (deleted.length === 0) return await interaction.editReply({
120 embeds: [
pineafan4edb7762022-06-26 19:21:04 +0100121 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000122 .setEmoji("CHANNEL.PURGE.RED")
123 .setTitle("Purge")
124 .setDescription("No messages were deleted")
125 .setStatus("Danger")
126 ],
127 components: []
pineafan63fc5e22022-08-04 22:04:10 +0100128 });
pineafan4edb7762022-06-26 19:21:04 +0100129 if (user) {
pineafan63fc5e22022-08-04 22:04:10 +0100130 await client.database.history.create("purge", interaction.guild.id, user, interaction.options.getString("reason"), null, null, deleted.length);
pineafan4edb7762022-06-26 19:21:04 +0100131 }
pineafan63fc5e22022-08-04 22:04:10 +0100132 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
133 const data = {
134 meta: {
135 type: "channelPurge",
136 displayName: "Channel Purged",
137 calculateType: "messageDelete",
138 color: NucleusColors.red,
139 emoji: "PUNISH.BAN.RED",
140 timestamp: new Date().getTime()
141 },
142 list: {
143 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
144 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
145 channel: entry(interaction.channel.id, renderChannel(interaction.channel)),
146 messagesCleared: entry(deleted.length, deleted.length)
147 },
148 hidden: {
149 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100150 }
pineafan63fc5e22022-08-04 22:04:10 +0100151 };
152 log(data);
153 let out = "";
154 deleted.reverse().forEach(message => {
155 out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date(message.createdTimestamp).toISOString()}]\n`;
156 const lines = message.content.split("\n");
157 lines.forEach(line => {out += `> ${line}\n`;});
158 out += "\n\n";
159 });
160 const attachmentObject = {
161 attachment: Buffer.from(out),
162 name: `purge-${channel.id}-${Date.now()}.txt`,
163 description: "Purge log"
164 };
165 const m = await interaction.editReply({embeds: [new EmojiEmbed()
166 .setEmoji("CHANNEL.PURGE.GREEN")
167 .setTitle("Purge")
pineafan5d1908e2022-02-28 21:34:47 +0000168 .setDescription("Messages cleared")
169 .setStatus("Success")
170 ], components: [new Discord.MessageActionRow().addComponents([
171 new Discord.MessageButton()
172 .setCustomId("download")
173 .setLabel("Download transcript")
174 .setStyle("SUCCESS")
175 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
pineafanbd02b4a2022-08-05 22:01:38 +0100176 ])]}) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000177 let component;
178 try {
pineafanbd02b4a2022-08-05 22:01:38 +0100179 component = await m.awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan63fc5e22022-08-04 22:04:10 +0100180 } catch { return; }
pineafan5d1908e2022-02-28 21:34:47 +0000181 if (component && component.customId === "download") {
pineafan4edb7762022-06-26 19:21:04 +0100182 interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000183 .setEmoji("CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100184 .setTitle("Purge")
pineafan5d1908e2022-02-28 21:34:47 +0000185 .setDescription("Uploaded")
186 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100187 ], components: [], files: [attachmentObject]});
pineafan5d1908e2022-02-28 21:34:47 +0000188 } else {
pineafan4edb7762022-06-26 19:21:04 +0100189 interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000190 .setEmoji("CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100191 .setTitle("Purge")
pineafan5d1908e2022-02-28 21:34:47 +0000192 .setDescription("Messages cleared")
193 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100194 ], components: []});
pineafan5d1908e2022-02-28 21:34:47 +0000195 }
pineafan63fc5e22022-08-04 22:04:10 +0100196 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000197 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100198 const confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000199 .setEmoji("CHANNEL.PURGE.RED")
200 .setTitle("Purge")
201 .setDescription(keyValueList({
pineafane625d782022-05-09 18:04:32 +0100202 "channel": `<#${channel.id}>`,
pineafan8b4b17f2022-02-27 20:42:52 +0000203 "amount": interaction.options.getInteger("amount").toString(),
204 "reason": `\n> ${interaction.options.getString("reason") ? interaction.options.getString("reason") : "*No reason provided*"}`
205 }))
206 .setColor("Danger")
pineafan63fc5e22022-08-04 22:04:10 +0100207 .send();
208 if (confirmation.cancelled) return;
pineafan377794f2022-04-18 19:01:01 +0100209 if (confirmation.success) {
pineafan5d1908e2022-02-28 21:34:47 +0000210 let messages;
pineafan8b4b17f2022-02-27 20:42:52 +0000211 try {
pineafan377794f2022-04-18 19:01:01 +0100212 if (!user) {
pineafan63fc5e22022-08-04 22:04:10 +0100213 const toDelete = await (interaction.channel as TextChannel).messages.fetch({limit: interaction.options.getInteger("amount")});
pineafan377794f2022-04-18 19:01:01 +0100214 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
215 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100216 const toDelete = (await (await (interaction.channel as TextChannel).messages.fetch({limit: 100}))
217 .filter(m => m.author.id === user.id)).first(interaction.options.getInteger("amount"));
pineafan377794f2022-04-18 19:01:01 +0100218 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
219 }
pineafan5d1908e2022-02-28 21:34:47 +0000220 } catch(e) {
pineafan4edb7762022-06-26 19:21:04 +0100221 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000222 .setEmoji("CHANNEL.PURGE.RED")
pineafan63fc5e22022-08-04 22:04:10 +0100223 .setTitle("Purge")
pineafan8b4b17f2022-02-27 20:42:52 +0000224 .setDescription("Something went wrong and no messages were deleted")
225 .setStatus("Danger")
pineafan63fc5e22022-08-04 22:04:10 +0100226 ], components: []});
pineafan8b4b17f2022-02-27 20:42:52 +0000227 }
pineafan4edb7762022-06-26 19:21:04 +0100228 if (user) {
pineafan63fc5e22022-08-04 22:04:10 +0100229 await client.database.history.create("purge", interaction.guild.id, user, interaction.options.getString("reason"), null, null, messages.size);
pineafan4edb7762022-06-26 19:21:04 +0100230 }
pineafan63fc5e22022-08-04 22:04:10 +0100231 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
232 const data = {
233 meta: {
234 type: "channelPurge",
235 displayName: "Channel Purged",
236 calculateType: "messageDelete",
237 color: NucleusColors.red,
238 emoji: "PUNISH.BAN.RED",
239 timestamp: new Date().getTime()
240 },
241 list: {
242 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
243 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
244 channel: entry(interaction.channel.id, renderChannel(interaction.channel)),
245 messagesCleared: entry(messages.size, messages.size)
246 },
247 hidden: {
248 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100249 }
pineafan63fc5e22022-08-04 22:04:10 +0100250 };
251 log(data);
252 let out = "";
253 messages.reverse().forEach(message => {
254 out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date(message.createdTimestamp).toISOString()}]\n`;
255 const lines = message.content.split("\n");
256 lines.forEach(line => {out += `> ${line}\n`;});
257 out += "\n\n";
258 });
259 const attachmentObject = {
260 attachment: Buffer.from(out),
261 name: `purge-${channel.id}-${Date.now()}.txt`,
262 description: "Purge log"
263 };
264 const m = await interaction.editReply({embeds: [new EmojiEmbed()
265 .setEmoji("CHANNEL.PURGE.GREEN")
266 .setTitle("Purge")
pineafan5d1908e2022-02-28 21:34:47 +0000267 .setDescription("Messages cleared")
268 .setStatus("Success")
269 ], components: [new Discord.MessageActionRow().addComponents([
270 new Discord.MessageButton()
271 .setCustomId("download")
272 .setLabel("Download transcript")
273 .setStyle("SUCCESS")
274 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
pineafanbd02b4a2022-08-05 22:01:38 +0100275 ])]}) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000276 let component;
277 try {
pineafanbd02b4a2022-08-05 22:01:38 +0100278 component = await m.awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 300000});
pineafan63fc5e22022-08-04 22:04:10 +0100279 } catch { return; }
pineafan5d1908e2022-02-28 21:34:47 +0000280 if (component && component.customId === "download") {
pineafan4edb7762022-06-26 19:21:04 +0100281 interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000282 .setEmoji("CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100283 .setTitle("Purge")
pineafan377794f2022-04-18 19:01:01 +0100284 .setDescription("Transcript uploaded above")
pineafan5d1908e2022-02-28 21:34:47 +0000285 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100286 ], components: [], files: [attachmentObject]});
pineafan5d1908e2022-02-28 21:34:47 +0000287 } else {
pineafan4edb7762022-06-26 19:21:04 +0100288 interaction.editReply({embeds: [new EmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000289 .setEmoji("CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100290 .setTitle("Purge")
pineafan5d1908e2022-02-28 21:34:47 +0000291 .setDescription("Messages cleared")
292 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100293 ], components: []});
pineafan5d1908e2022-02-28 21:34:47 +0000294 }
pineafan8b4b17f2022-02-27 20:42:52 +0000295 } else {
pineafan4edb7762022-06-26 19:21:04 +0100296 await interaction.editReply({embeds: [new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000297 .setEmoji("CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100298 .setTitle("Purge")
pineafan8b4b17f2022-02-27 20:42:52 +0000299 .setDescription("No changes were made")
300 .setStatus("Success")
pineafan63fc5e22022-08-04 22:04:10 +0100301 ], components: []});
pineafan8b4b17f2022-02-27 20:42:52 +0000302 }
303 }
pineafan63fc5e22022-08-04 22:04:10 +0100304};
pineafan4f164f32022-02-26 22:07:12 +0000305
pineafanbd02b4a2022-08-05 22:01:38 +0100306const check = (interaction: CommandInteraction) => {
pineafan63fc5e22022-08-04 22:04:10 +0100307 const member = (interaction.member as GuildMember);
308 const me = (interaction.guild.me as GuildMember);
pineafanc1c18792022-08-03 21:41:36 +0100309 // Check if nucleus has the manage_messages permission
310 if (! me.permissions.has("MANAGE_MESSAGES")) throw "I do not have the *Manage Messages* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000311 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100312 if (member.id === interaction.guild.ownerId) return true;
pineafan8b4b17f2022-02-27 20:42:52 +0000313 // Check if the user has manage_messages permission
pineafane23c4ec2022-07-27 21:56:27 +0100314 if (! member.permissions.has("MANAGE_MESSAGES")) throw "You do not have the *Manage Messages* permission";
pineafanc1c18792022-08-03 21:41:36 +0100315 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100316 return true;
317};
pineafan4f164f32022-02-26 22:07:12 +0000318
pineafan8b4b17f2022-02-27 20:42:52 +0000319export { command, callback, check };