blob: 8e6762ed3bfb65179a90dcda223513f7131649fa [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";
3import { WrappedCheck } from "jshaiku";
pineafan8b4b17f2022-02-27 20:42:52 +00004import confirmationMessage from "../../utils/confirmationMessage.js";
pineafan377794f2022-04-18 19:01:01 +01005import generateEmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +00006import keyValueList from "../../utils/generateKeyValueList.js";
7import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4f164f32022-02-26 22:07:12 +00008
9const command = (builder: SlashCommandSubcommandBuilder) =>
10 builder
11 .setName("purge")
pineafan8b4b17f2022-02-27 20:42:52 +000012 .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)
pineafan1dc15722022-03-14 21:27:34 +000018 .setMaxValue(100))
pineafan8b4b17f2022-02-27 20:42:52 +000019 .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
pineafan6702cef2022-06-13 17:52:37 +010022const callback = async (interaction: CommandInteraction): Promise<any> => {
pineafan5d1908e2022-02-28 21:34:47 +000023 let user = interaction.options.getMember("user") as GuildMember ?? null
pineafan377794f2022-04-18 19:01:01 +010024 let 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: [
pineafan377794f2022-04-18 19:01:01 +010028 new generateEmojiEmbed()
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: [],
35 ephemeral: true,
36 })
37 }
38 // TODO:[Modals] Replace this with a modal
39 if ( !interaction.options.getInteger("amount") ) {
40 await interaction.reply({
41 embeds: [
pineafan377794f2022-04-18 19:01:01 +010042 new generateEmojiEmbed()
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
51 })
pineafan5d1908e2022-02-28 21:34:47 +000052 let deleted = [] as Discord.Message[]
pineafan8b4b17f2022-02-27 20:42:52 +000053 while (true) {
54 let m = await interaction.editReply({
55 embeds: [
pineafan377794f2022-04-18 19:01:01 +010056 new generateEmojiEmbed()
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")
76 ]),
77 new Discord.MessageActionRow().addComponents([
78 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 ]
99 })
100 let component;
101 try {
102 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
103 } 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) {
111 ms = ms.filter(m => m.author.id === user.id)
112 }
113 messages = await (channel as TextChannel).bulkDelete(ms, true);
114 })
pineafane625d782022-05-09 18:04:32 +0100115 if (messages) {
116 deleted = deleted.concat(messages.map(m => m))
117 }
pineafan8b4b17f2022-02-27 20:42:52 +0000118 }
119 if (deleted.length === 0) return await interaction.editReply({
120 embeds: [
pineafan377794f2022-04-18 19:01:01 +0100121 new generateEmojiEmbed()
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: []
128 })
pineafan5d1908e2022-02-28 21:34:47 +0000129 let attachmentObject;
130 try {
pineafane625d782022-05-09 18:04:32 +0100131 // @ts-ignore
132 const { log, NucleusColors, entry, renderUser, renderChannel } = interaction.user.client.logger
133 let 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 id: 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
150 }
151 }
152 log(data, interaction.user.client);
pineafan5d1908e2022-02-28 21:34:47 +0000153 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 let lines = message.content.split("\n")
157 lines.forEach(line => {out += `> ${line}\n`})
158 out += `\n\n`
159 })
160 attachmentObject = {
161 attachment: Buffer.from(out),
162 name: `purge-${channel.id}-${Date.now()}.txt`,
163 description: "Purge log"
164 }
165 } catch {}
pineafan377794f2022-04-18 19:01:01 +0100166 let m = await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000167 .setEmoji(`CHANNEL.PURGE.GREEN`)
168 .setTitle(`Purge`)
169 .setDescription("Messages cleared")
170 .setStatus("Success")
171 ], components: [new Discord.MessageActionRow().addComponents([
172 new Discord.MessageButton()
173 .setCustomId("download")
174 .setLabel("Download transcript")
175 .setStyle("SUCCESS")
176 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
177 ])]})
178 let component;
179 try {
180 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
181 } catch {}
182 if (component && component.customId === "download") {
pineafan377794f2022-04-18 19:01:01 +0100183 interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000184 .setEmoji("CHANNEL.PURGE.GREEN")
185 .setTitle(`Purge`)
186 .setDescription("Uploaded")
187 .setStatus("Success")
188 ], components: [], files: [attachmentObject]})
189 } else {
pineafan377794f2022-04-18 19:01:01 +0100190 interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000191 .setEmoji("CHANNEL.PURGE.GREEN")
192 .setTitle(`Purge`)
193 .setDescription("Messages cleared")
194 .setStatus("Success")
195 ], components: []})
196 }
197 return
pineafan8b4b17f2022-02-27 20:42:52 +0000198 } else {
pineafan377794f2022-04-18 19:01:01 +0100199 let confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000200 .setEmoji("CHANNEL.PURGE.RED")
201 .setTitle("Purge")
202 .setDescription(keyValueList({
pineafane625d782022-05-09 18:04:32 +0100203 "channel": `<#${channel.id}>`,
pineafan8b4b17f2022-02-27 20:42:52 +0000204 "amount": interaction.options.getInteger("amount").toString(),
205 "reason": `\n> ${interaction.options.getString("reason") ? interaction.options.getString("reason") : "*No reason provided*"}`
206 }))
207 .setColor("Danger")
pineafan377794f2022-04-18 19:01:01 +0100208 .send()
209 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) {
213 let toDelete = await (interaction.channel as TextChannel).messages.fetch({limit: interaction.options.getInteger("amount")})
214 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
215 } else {
216 let toDelete = (await (await (interaction.channel as TextChannel).messages.fetch({limit: 100}))
217 .filter(m => m.author.id === user.id)).first(interaction.options.getInteger("amount"))
218 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
219 }
pineafan5d1908e2022-02-28 21:34:47 +0000220 } catch(e) {
pineafan377794f2022-04-18 19:01:01 +0100221 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000222 .setEmoji("CHANNEL.PURGE.RED")
223 .setTitle(`Purge`)
224 .setDescription("Something went wrong and no messages were deleted")
225 .setStatus("Danger")
226 ], components: []})
227 }
pineafan5d1908e2022-02-28 21:34:47 +0000228 let attachmentObject;
229 try {
pineafane625d782022-05-09 18:04:32 +0100230 // @ts-ignore
231 const { log, NucleusColors, entry, renderUser, renderChannel } = interaction.user.client.logger
232 let 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 id: 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
249 }
250 }
251 log(data, interaction.user.client);
pineafan5d1908e2022-02-28 21:34:47 +0000252 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 let lines = message.content.split("\n")
256 lines.forEach(line => {out += `> ${line}\n`})
257 out += `\n\n`
258 })
259 attachmentObject = {
260 attachment: Buffer.from(out),
261 name: `purge-${channel.id}-${Date.now()}.txt`,
262 description: `Purge log`
263 }
264 } catch {}
pineafan377794f2022-04-18 19:01:01 +0100265 let m = await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000266 .setEmoji(`CHANNEL.PURGE.GREEN`)
267 .setTitle(`Purge`)
268 .setDescription("Messages cleared")
269 .setStatus("Success")
270 ], components: [new Discord.MessageActionRow().addComponents([
271 new Discord.MessageButton()
272 .setCustomId("download")
273 .setLabel("Download transcript")
274 .setStyle("SUCCESS")
275 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
276 ])]})
277 let component;
278 try {
279 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
280 } catch {}
281 if (component && component.customId === "download") {
pineafan377794f2022-04-18 19:01:01 +0100282 interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000283 .setEmoji("CHANNEL.PURGE.GREEN")
284 .setTitle(`Purge`)
pineafan377794f2022-04-18 19:01:01 +0100285 .setDescription("Transcript uploaded above")
pineafan5d1908e2022-02-28 21:34:47 +0000286 .setStatus("Success")
287 ], components: [], files: [attachmentObject]})
288 } else {
pineafan377794f2022-04-18 19:01:01 +0100289 interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan5d1908e2022-02-28 21:34:47 +0000290 .setEmoji("CHANNEL.PURGE.GREEN")
291 .setTitle(`Purge`)
292 .setDescription("Messages cleared")
293 .setStatus("Success")
294 ], components: []})
295 }
pineafan8b4b17f2022-02-27 20:42:52 +0000296 } else {
pineafan377794f2022-04-18 19:01:01 +0100297 await interaction.editReply({embeds: [new generateEmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +0000298 .setEmoji("CHANNEL.PURGE.GREEN")
299 .setTitle(`Purge`)
300 .setDescription("No changes were made")
301 .setStatus("Success")
302 ], components: []})
303 }
304 }
pineafan4f164f32022-02-26 22:07:12 +0000305}
306
307const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan663dc472022-05-10 18:13:47 +0100308 let member = (interaction.member as GuildMember)
PineappleFan5fe720d2022-05-19 12:01:49 +0100309 let me = (interaction.guild.me as GuildMember)
pineafan8b4b17f2022-02-27 20:42:52 +0000310 // Allow the owner to purge
pineafan663dc472022-05-10 18:13:47 +0100311 if (member.id == interaction.guild.ownerId) return true
pineafan8b4b17f2022-02-27 20:42:52 +0000312 // Check if the user has manage_messages permission
pineafan663dc472022-05-10 18:13:47 +0100313 if (! member.permissions.has("MANAGE_MESSAGES")) throw "You do not have the `manage_messages` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000314 // Check if nucleus has the manage_messages permission
PineappleFan5fe720d2022-05-19 12:01:49 +0100315 if (! me.permissions.has("MANAGE_MESSAGES")) throw "I do not have the `manage_messages` permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000316 // Allow warn
317 return true
pineafan4f164f32022-02-26 22:07:12 +0000318}
319
pineafan8b4b17f2022-02-27 20:42:52 +0000320export { command, callback, check };