blob: 1dc75ebe57f68972da7ccb129f53c359a5c636d3 [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";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import 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)
18 .setMaxValue(50))
19 .addChannelOption(option => option.setName("channel").setDescription("The channel to purge messages from").setRequired(false))
20 .addUserOption(option => option.setName("user").setDescription("The user to purge messages from").setRequired(false))
21 .addStringOption(option => option.setName("reason").setDescription("The reason for the purge").setRequired(false))
pineafan4f164f32022-02-26 22:07:12 +000022
pineafan8b4b17f2022-02-27 20:42:52 +000023const callback = async (interaction: CommandInteraction) => {
pineafan5d1908e2022-02-28 21:34:47 +000024 let user = interaction.options.getMember("user") as GuildMember ?? null
pineafan8b4b17f2022-02-27 20:42:52 +000025 let channel = (interaction.options.getChannel("channel") as GuildChannel) ?? interaction.channel
26 let thischannel
27 if ((interaction.options.getChannel("channel") as GuildChannel) == null) {
28 thischannel = true
29 } else {
30 thischannel = (interaction.options.getChannel("channel") as GuildChannel).id == interaction.channel.id
31 }
32 if (!(["GUILD_TEXT", "GUILD_NEWS", "GUILD_NEWS_THREAD", "GUILD_PUBLIC_THREAD", "GUILD_PRIVATE_THREAD"].includes(channel.type.toString()))) {
33 return await interaction.reply({
34 embeds: [
35 new EmojiEmbed()
36 .setEmoji("CHANNEL.PURGE.RED")
37 .setTitle("Purge")
38 .setDescription("You cannot purge this channel")
39 .setStatus("Danger")
40 ],
41 components: [],
42 ephemeral: true,
43 })
44 }
45 // TODO:[Modals] Replace this with a modal
46 if ( !interaction.options.getInteger("amount") ) {
47 await interaction.reply({
48 embeds: [
49 new EmojiEmbed()
50 .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
58 })
pineafan5d1908e2022-02-28 21:34:47 +000059 let deleted = [] as Discord.Message[]
pineafan8b4b17f2022-02-27 20:42:52 +000060 while (true) {
61 let m = await interaction.editReply({
62 embeds: [
63 new EmojiEmbed()
64 .setEmoji("CHANNEL.PURGE.RED")
65 .setTitle("Purge")
66 .setDescription("Select how many messages to delete. You can continue clicking until all messages are cleared.")
67 .setStatus("Danger")
68 ],
69 components: [
70 new Discord.MessageActionRow().addComponents([
71 new Discord.MessageButton()
72 .setCustomId("1")
73 .setLabel("1")
74 .setStyle("SECONDARY"),
75 new Discord.MessageButton()
76 .setCustomId("3")
77 .setLabel("3")
78 .setStyle("SECONDARY"),
79 new Discord.MessageButton()
80 .setCustomId("5")
81 .setLabel("5")
82 .setStyle("SECONDARY")
83 ]),
84 new Discord.MessageActionRow().addComponents([
85 new Discord.MessageButton()
86 .setCustomId("10")
87 .setLabel("10")
88 .setStyle("SECONDARY"),
89 new Discord.MessageButton()
90 .setCustomId("25")
91 .setLabel("25")
92 .setStyle("SECONDARY"),
93 new Discord.MessageButton()
94 .setCustomId("50")
95 .setLabel("50")
96 .setStyle("SECONDARY")
97 ]),
98 new Discord.MessageActionRow().addComponents([
99 new Discord.MessageButton()
100 .setCustomId("done")
101 .setLabel("Done")
102 .setStyle("SUCCESS")
103 .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
104 ])
105 ]
106 })
107 let component;
108 try {
109 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
110 } catch (e) { break; }
111 component.deferUpdate();
112 if (component.customId === "done") break;
113 let amount;
114 try { amount = parseInt(component.customId); } catch { break; }
pineafan5d1908e2022-02-28 21:34:47 +0000115 let messages;
116 (interaction.channel as TextChannel).messages.fetch({limit: amount}).then(async (ms) => {
117 if (user) {
118 ms = ms.filter(m => m.author.id === user.id)
119 }
120 messages = await (channel as TextChannel).bulkDelete(ms, true);
121 })
122 deleted = deleted.concat(messages.map(m => m)) // TODO: .values doesnt work so using .map
123 // TODO: Support for users
pineafan8b4b17f2022-02-27 20:42:52 +0000124 }
125 if (deleted.length === 0) return await interaction.editReply({
126 embeds: [
127 new EmojiEmbed()
128 .setEmoji("CHANNEL.PURGE.RED")
129 .setTitle("Purge")
130 .setDescription("No messages were deleted")
131 .setStatus("Danger")
132 ],
133 components: []
134 })
pineafan5d1908e2022-02-28 21:34:47 +0000135 let attachmentObject;
136 try {
137 let out = ""
138 deleted.reverse().forEach(message => {
139 out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date(message.createdTimestamp).toISOString()}]\n`
140 let lines = message.content.split("\n")
141 lines.forEach(line => {out += `> ${line}\n`})
142 out += `\n\n`
143 })
144 attachmentObject = {
145 attachment: Buffer.from(out),
146 name: `purge-${channel.id}-${Date.now()}.txt`,
147 description: "Purge log"
148 }
149 } catch {}
150 let m = await interaction.editReply({embeds: [new EmojiEmbed()
151 .setEmoji(`CHANNEL.PURGE.GREEN`)
152 .setTitle(`Purge`)
153 .setDescription("Messages cleared")
154 .setStatus("Success")
155 ], components: [new Discord.MessageActionRow().addComponents([
156 new Discord.MessageButton()
157 .setCustomId("download")
158 .setLabel("Download transcript")
159 .setStyle("SUCCESS")
160 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
161 ])]})
162 let component;
163 try {
164 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
165 } catch {}
166 if (component && component.customId === "download") {
167 interaction.editReply({embeds: [new EmojiEmbed()
168 .setEmoji("CHANNEL.PURGE.GREEN")
169 .setTitle(`Purge`)
170 .setDescription("Uploaded")
171 .setStatus("Success")
172 ], components: [], files: [attachmentObject]})
173 } else {
174 interaction.editReply({embeds: [new EmojiEmbed()
175 .setEmoji("CHANNEL.PURGE.GREEN")
176 .setTitle(`Purge`)
177 .setDescription("Messages cleared")
178 .setStatus("Success")
179 ], components: []})
180 }
181 return
pineafan8b4b17f2022-02-27 20:42:52 +0000182 } else {
183 if (await new confirmationMessage(interaction)
184 .setEmoji("CHANNEL.PURGE.RED")
185 .setTitle("Purge")
186 .setDescription(keyValueList({
187 "channel": `<#${channel.id}> (${(channel as GuildChannel).name})` + (thischannel ? " [This channel]" : ""),
188 "amount": interaction.options.getInteger("amount").toString(),
189 "reason": `\n> ${interaction.options.getString("reason") ? interaction.options.getString("reason") : "*No reason provided*"}`
190 }))
191 .setColor("Danger")
192 // pluralize("day", interaction.options.getInteger("amount"))
193 // const pluralize = (word: string, count: number) => { return count === 1 ? word : word + "s" }
194 .send()) {
pineafan5d1908e2022-02-28 21:34:47 +0000195 let messages;
pineafan8b4b17f2022-02-27 20:42:52 +0000196 try {
pineafan5d1908e2022-02-28 21:34:47 +0000197 (interaction.channel as TextChannel).messages.fetch({limit: interaction.options.getInteger("amount")}).then(async (ms) => {
198 if (user) {
199 ms = ms.filter(m => m.author.id === user.id)
200 }
201 messages = await (channel as TextChannel).bulkDelete(ms, true);
202 }) // TODO: fix for purge amount by user, not just checking x
203 } catch(e) {
204 console.log(e)
pineafan8b4b17f2022-02-27 20:42:52 +0000205 await interaction.editReply({embeds: [new EmojiEmbed()
206 .setEmoji("CHANNEL.PURGE.RED")
207 .setTitle(`Purge`)
208 .setDescription("Something went wrong and no messages were deleted")
209 .setStatus("Danger")
210 ], components: []})
211 }
pineafan5d1908e2022-02-28 21:34:47 +0000212 let attachmentObject;
213 try {
214 let out = ""
215 messages.reverse().forEach(message => {
216 out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date(message.createdTimestamp).toISOString()}]\n`
217 let lines = message.content.split("\n")
218 lines.forEach(line => {out += `> ${line}\n`})
219 out += `\n\n`
220 })
221 attachmentObject = {
222 attachment: Buffer.from(out),
223 name: `purge-${channel.id}-${Date.now()}.txt`,
224 description: `Purge log`
225 }
226 } catch {}
227 let m = await interaction.editReply({embeds: [new EmojiEmbed()
228 .setEmoji(`CHANNEL.PURGE.GREEN`)
229 .setTitle(`Purge`)
230 .setDescription("Messages cleared")
231 .setStatus("Success")
232 ], components: [new Discord.MessageActionRow().addComponents([
233 new Discord.MessageButton()
234 .setCustomId("download")
235 .setLabel("Download transcript")
236 .setStyle("SUCCESS")
237 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
238 ])]})
239 let component;
240 try {
241 component = await (m as Discord.Message).awaitMessageComponent({filter: (m) => m.user.id === interaction.user.id, time: 2.5 * 60 * 1000});
242 } catch {}
243 if (component && component.customId === "download") {
244 interaction.editReply({embeds: [new EmojiEmbed()
245 .setEmoji("CHANNEL.PURGE.GREEN")
246 .setTitle(`Purge`)
247 .setDescription("Uploaded")
248 .setStatus("Success")
249 ], components: [], files: [attachmentObject]})
250 } else {
251 interaction.editReply({embeds: [new EmojiEmbed()
252 .setEmoji("CHANNEL.PURGE.GREEN")
253 .setTitle(`Purge`)
254 .setDescription("Messages cleared")
255 .setStatus("Success")
256 ], components: []})
257 }
pineafan8b4b17f2022-02-27 20:42:52 +0000258 } else {
259 await interaction.editReply({embeds: [new EmojiEmbed()
260 .setEmoji("CHANNEL.PURGE.GREEN")
261 .setTitle(`Purge`)
262 .setDescription("No changes were made")
263 .setStatus("Success")
264 ], components: []})
265 }
266 }
pineafan4f164f32022-02-26 22:07:12 +0000267}
268
269const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => {
pineafan8b4b17f2022-02-27 20:42:52 +0000270 // Allow the owner to purge
271 if ((interaction.member as GuildMember).id == interaction.guild.ownerId) return true
272 // Check if the user has manage_messages permission
273 if (! (interaction.member as GuildMember).permissions.has("MANAGE_MESSAGES")) throw "You do not have the `manage_messages` permission";
274 // Check if nucleus has the manage_messages permission
275 if (! interaction.guild.me.permissions.has("MANAGE_MESSAGES")) throw "I do not have the `manage_messages` permission";
276 // Allow warn
277 return true
pineafan4f164f32022-02-26 22:07:12 +0000278}
279
pineafan8b4b17f2022-02-27 20:42:52 +0000280export { command, callback, check };