blob: c5fe2fb982f31bce7ac8e17961770ab572826bbb [file] [log] [blame]
Skyler Greyda16adf2023-03-05 10:22:12 +00001import Discord, {
2 CommandInteraction,
3 GuildChannel,
4 GuildMember,
5 TextChannel,
6 ButtonStyle,
7 ButtonBuilder,
8 Message
9} from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -050010import type { SlashCommandSubcommandBuilder } from "discord.js";
pineafan8b4b17f2022-02-27 20:42:52 +000011import confirmationMessage from "../../utils/confirmationMessage.js";
pineafan4edb7762022-06-26 19:21:04 +010012import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafan8b4b17f2022-02-27 20:42:52 +000013import keyValueList from "../../utils/generateKeyValueList.js";
14import getEmojiByName from "../../utils/getEmojiByName.js";
pineafan4edb7762022-06-26 19:21:04 +010015import client from "../../utils/client.js";
pineafan4f164f32022-02-26 22:07:12 +000016
17const command = (builder: SlashCommandSubcommandBuilder) =>
18 builder
pineafan63fc5e22022-08-04 22:04:10 +010019 .setName("purge")
20 .setDescription("Bulk deletes messages in a channel")
Skyler Grey75ea9172022-08-06 10:22:23 +010021 .addIntegerOption((option) =>
22 option
23 .setName("amount")
24 .setDescription("The amount of messages to delete")
25 .setRequired(false)
26 .setMinValue(1)
27 .setMaxValue(100)
28 )
29 .addUserOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010030 option.setName("user").setDescription("The user to purge messages from").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010031 )
32 .addStringOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010033 option.setName("reason").setDescription("The reason for the purge").setRequired(false)
Skyler Grey75ea9172022-08-06 10:22:23 +010034 );
pineafan4f164f32022-02-26 22:07:12 +000035
pineafan3a02ea32022-08-11 21:35:04 +010036const callback = async (interaction: CommandInteraction): Promise<unknown> => {
PineaFana34d04b2023-01-03 22:05:42 +000037 if (!interaction.guild) return;
Skyler Greyda16adf2023-03-05 10:22:12 +000038 const user = interaction.options.getMember("user") as GuildMember | null;
Skyler Grey75ea9172022-08-06 10:22:23 +010039 const channel = interaction.channel as GuildChannel;
TheCodedProf088b1b22023-02-28 17:31:11 -050040 if (!channel.isTextBased()) {
pineafan8b4b17f2022-02-27 20:42:52 +000041 return await interaction.reply({
42 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010043 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000044 .setEmoji("CHANNEL.PURGE.RED")
45 .setTitle("Purge")
46 .setDescription("You cannot purge this channel")
47 .setStatus("Danger")
48 ],
49 components: [],
pineafan63fc5e22022-08-04 22:04:10 +010050 ephemeral: true
51 });
pineafan8b4b17f2022-02-27 20:42:52 +000052 }
53 // TODO:[Modals] Replace this with a modal
PineaFana34d04b2023-01-03 22:05:42 +000054 if (!interaction.options.get("amount")) {
pineafan8b4b17f2022-02-27 20:42:52 +000055 await interaction.reply({
56 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010057 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000058 .setEmoji("CHANNEL.PURGE.RED")
59 .setTitle("Purge")
60 .setDescription("Select how many messages to delete")
61 .setStatus("Danger")
62 ],
63 components: [],
64 ephemeral: true,
65 fetchReply: true
pineafan63fc5e22022-08-04 22:04:10 +010066 });
67 let deleted = [] as Discord.Message[];
Skyler Greyad002172022-08-16 18:48:26 +010068 let timedOut = false;
69 let amountSelected = false;
70 while (!timedOut && !amountSelected) {
Skyler Grey75ea9172022-08-06 10:22:23 +010071 const m = (await interaction.editReply({
pineafan8b4b17f2022-02-27 20:42:52 +000072 embeds: [
pineafan4edb7762022-06-26 19:21:04 +010073 new EmojiEmbed()
pineafan8b4b17f2022-02-27 20:42:52 +000074 .setEmoji("CHANNEL.PURGE.RED")
75 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +010076 .setDescription(
77 "Select how many messages to delete. You can continue clicking until all messages are cleared."
78 )
pineafan8b4b17f2022-02-27 20:42:52 +000079 .setStatus("Danger")
80 ],
81 components: [
PineaFana34d04b2023-01-03 22:05:42 +000082 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040083 new Discord.ButtonBuilder().setCustomId("1").setLabel("1").setStyle(ButtonStyle.Secondary),
84 new Discord.ButtonBuilder().setCustomId("3").setLabel("3").setStyle(ButtonStyle.Secondary),
85 new Discord.ButtonBuilder().setCustomId("5").setLabel("5").setStyle(ButtonStyle.Secondary)
pineafan63fc5e22022-08-04 22:04:10 +010086 ]),
PineaFana34d04b2023-01-03 22:05:42 +000087 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040088 new Discord.ButtonBuilder().setCustomId("10").setLabel("10").setStyle(ButtonStyle.Secondary),
89 new Discord.ButtonBuilder().setCustomId("25").setLabel("25").setStyle(ButtonStyle.Secondary),
90 new Discord.ButtonBuilder().setCustomId("50").setLabel("50").setStyle(ButtonStyle.Secondary)
pineafan8b4b17f2022-02-27 20:42:52 +000091 ]),
PineaFana34d04b2023-01-03 22:05:42 +000092 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -040093 new Discord.ButtonBuilder()
pineafan8b4b17f2022-02-27 20:42:52 +000094 .setCustomId("done")
95 .setLabel("Done")
TheCodedProf21c08592022-09-13 14:14:43 -040096 .setStyle(ButtonStyle.Success)
pineafan8b4b17f2022-02-27 20:42:52 +000097 .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
98 ])
99 ]
Skyler Grey75ea9172022-08-06 10:22:23 +0100100 })) as Discord.Message;
pineafan8b4b17f2022-02-27 20:42:52 +0000101 let component;
102 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100103 component = m.awaitMessageComponent({
Skyler Greyda16adf2023-03-05 10:22:12 +0000104 filter: (i) =>
105 i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
Skyler Grey75ea9172022-08-06 10:22:23 +0100106 time: 300000
107 });
108 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100109 timedOut = true;
110 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100111 }
TheCodedProf21c08592022-09-13 14:14:43 -0400112 (await component).deferUpdate();
113 if ((await component).customId === "done") {
Skyler Greyad002172022-08-16 18:48:26 +0100114 amountSelected = true;
115 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100116 }
TheCodedProf21c08592022-09-13 14:14:43 -0400117 const amount = parseInt((await component).customId);
Skyler Greyad002172022-08-16 18:48:26 +0100118
PineaFana34d04b2023-01-03 22:05:42 +0000119 let messages: Discord.Message[] = [];
Skyler Grey11236ba2022-08-08 21:13:33 +0100120 await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => {
121 if (user) {
122 ms = ms.filter((m) => m.author.id === user.id);
123 }
Skyler Greyda16adf2023-03-05 10:22:12 +0000124 messages = (await (channel as TextChannel).bulkDelete(ms, true)).map((m) => m as Discord.Message);
Skyler Grey11236ba2022-08-08 21:13:33 +0100125 });
PineaFana34d04b2023-01-03 22:05:42 +0000126 deleted = deleted.concat(messages);
pineafan8b4b17f2022-02-27 20:42:52 +0000127 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100128 if (deleted.length === 0)
129 return await interaction.editReply({
130 embeds: [
131 new EmojiEmbed()
132 .setEmoji("CHANNEL.PURGE.RED")
133 .setTitle("Purge")
134 .setDescription("No messages were deleted")
135 .setStatus("Danger")
136 ],
137 components: []
138 });
pineafan4edb7762022-06-26 19:21:04 +0100139 if (user) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100140 await client.database.history.create(
141 "purge",
142 interaction.guild.id,
PineaFana34d04b2023-01-03 22:05:42 +0000143 user.user,
144 interaction.user,
Skyler Greyda16adf2023-03-05 10:22:12 +0000145 (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*",
Skyler Grey75ea9172022-08-06 10:22:23 +0100146 null,
147 null,
PineaFana34d04b2023-01-03 22:05:42 +0000148 deleted.length.toString()
Skyler Grey75ea9172022-08-06 10:22:23 +0100149 );
pineafan4edb7762022-06-26 19:21:04 +0100150 }
Skyler Grey11236ba2022-08-08 21:13:33 +0100151 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +0100152 const data = {
153 meta: {
154 type: "channelPurge",
155 displayName: "Channel Purged",
156 calculateType: "messageDelete",
157 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000158 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500159 timestamp: Date.now()
pineafan63fc5e22022-08-04 22:04:10 +0100160 },
161 list: {
Skyler Grey11236ba2022-08-08 21:13:33 +0100162 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
163 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
PineaFana34d04b2023-01-03 22:05:42 +0000164 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
165 messagesCleared: entry(deleted.length.toString(), deleted.length.toString())
pineafan63fc5e22022-08-04 22:04:10 +0100166 },
167 hidden: {
168 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100169 }
pineafan63fc5e22022-08-04 22:04:10 +0100170 };
171 log(data);
Skyler Greyda16adf2023-03-05 10:22:12 +0000172 const newOut = await client.database.transcripts.createTranscript(
Skyler Greye0c511b2023-03-06 10:30:17 +0000173 "purge",
Skyler Greyda16adf2023-03-05 10:22:12 +0000174 deleted,
175 interaction,
176 interaction.member as GuildMember
177 );
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500178 const transcript = client.database.transcripts.toHumanReadable(newOut);
pineafan63fc5e22022-08-04 22:04:10 +0100179 const attachmentObject = {
PineaFan0d06edc2023-01-17 22:10:31 +0000180 attachment: Buffer.from(transcript),
pineafan63fc5e22022-08-04 22:04:10 +0100181 name: `purge-${channel.id}-${Date.now()}.txt`,
182 description: "Purge log"
183 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100184 const m = (await interaction.editReply({
185 embeds: [
186 new EmojiEmbed()
187 .setEmoji("CHANNEL.PURGE.GREEN")
188 .setTitle("Purge")
189 .setDescription("Messages cleared")
190 .setStatus("Success")
191 ],
192 components: [
PineaFana34d04b2023-01-03 22:05:42 +0000193 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -0400194 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100195 .setCustomId("download")
196 .setLabel("Download transcript")
TheCodedProf21c08592022-09-13 14:14:43 -0400197 .setStyle(ButtonStyle.Success)
Skyler Grey75ea9172022-08-06 10:22:23 +0100198 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
199 ])
200 ]
201 })) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000202 let component;
203 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100204 component = await m.awaitMessageComponent({
Skyler Greyda16adf2023-03-05 10:22:12 +0000205 filter: (i) =>
206 i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
Skyler Grey75ea9172022-08-06 10:22:23 +0100207 time: 300000
208 });
209 } catch {
210 return;
211 }
PineaFana34d04b2023-01-03 22:05:42 +0000212 if (component.customId === "download") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100213 interaction.editReply({
214 embeds: [
215 new EmojiEmbed()
216 .setEmoji("CHANNEL.PURGE.GREEN")
217 .setTitle("Purge")
218 .setDescription("Uploaded")
219 .setStatus("Success")
220 ],
221 components: [],
222 files: [attachmentObject]
223 });
pineafan5d1908e2022-02-28 21:34:47 +0000224 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100225 interaction.editReply({
226 embeds: [
227 new EmojiEmbed()
228 .setEmoji("CHANNEL.PURGE.GREEN")
229 .setTitle("Purge")
230 .setDescription("Messages cleared")
231 .setStatus("Success")
232 ],
233 components: []
234 });
pineafan5d1908e2022-02-28 21:34:47 +0000235 }
pineafan63fc5e22022-08-04 22:04:10 +0100236 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000237 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100238 const confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000239 .setEmoji("CHANNEL.PURGE.RED")
240 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +0100241 .setDescription(
242 keyValueList({
243 channel: `<#${channel.id}>`,
PineaFana34d04b2023-01-03 22:05:42 +0000244 amount: (interaction.options.get("amount")?.value as number).toString(),
Skyler Greyda16adf2023-03-05 10:22:12 +0000245 reason: `\n> ${
246 interaction.options.get("reason")?.value
247 ? interaction.options.get("reason")?.value
248 : "*No reason provided*"
249 }`
Skyler Grey75ea9172022-08-06 10:22:23 +0100250 })
251 )
pineafan8b4b17f2022-02-27 20:42:52 +0000252 .setColor("Danger")
PineaFan1dee28f2023-01-16 22:09:07 +0000253 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100254 .send();
PineaFan1dee28f2023-01-16 22:09:07 +0000255 if (confirmation.cancelled || !confirmation.success) return;
256 let messages;
257 try {
258 if (!user) {
259 const toDelete = await (interaction.channel as TextChannel).messages.fetch({
260 limit: interaction.options.get("amount")?.value as number
Skyler Grey75ea9172022-08-06 10:22:23 +0100261 });
PineaFan1dee28f2023-01-16 22:09:07 +0000262 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
263 } else {
264 const toDelete = (
265 await (
266 await (interaction.channel as TextChannel).messages.fetch({
267 limit: 100
268 })
269 ).filter((m) => m.author.id === user.id)
270 ).first(interaction.options.get("amount")?.value as number);
271 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
pineafan8b4b17f2022-02-27 20:42:52 +0000272 }
PineaFan1dee28f2023-01-16 22:09:07 +0000273 } catch (e) {
274 await interaction.editReply({
275 embeds: [
276 new EmojiEmbed()
277 .setEmoji("CHANNEL.PURGE.RED")
278 .setTitle("Purge")
279 .setDescription("Something went wrong and no messages were deleted")
280 .setStatus("Danger")
281 ],
282 components: []
pineafan63fc5e22022-08-04 22:04:10 +0100283 });
PineaFan1dee28f2023-01-16 22:09:07 +0000284 }
285 if (!messages) {
286 await interaction.editReply({
287 embeds: [
288 new EmojiEmbed()
289 .setEmoji("CHANNEL.PURGE.RED")
290 .setTitle("Purge")
291 .setDescription("No messages could be deleted")
292 .setStatus("Danger")
293 ],
294 components: []
295 });
296 return;
297 }
298 if (user) {
299 await client.database.history.create(
300 "purge",
301 interaction.guild.id,
302 user.user,
303 interaction.user,
Skyler Greyda16adf2023-03-05 10:22:12 +0000304 (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*",
PineaFan1dee28f2023-01-16 22:09:07 +0000305 null,
306 null,
307 messages.size.toString()
308 );
309 }
310 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
311 const data = {
312 meta: {
313 type: "channelPurge",
314 displayName: "Channel Purged",
315 calculateType: "messageDelete",
316 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000317 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500318 timestamp: Date.now()
PineaFan1dee28f2023-01-16 22:09:07 +0000319 },
320 list: {
321 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
322 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
323 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
324 messagesCleared: entry(messages.size.toString(), messages.size.toString())
325 },
326 hidden: {
327 guild: interaction.guild.id
328 }
329 };
330 log(data);
Skyler Greyda16adf2023-03-05 10:22:12 +0000331 const messageArray: Message[] = messages
332 .filter(
333 (message) =>
334 !message!.components.some((component) =>
335 component.components.some((child) => child.customId?.includes("transcript") ?? false)
336 )
pineafande618c42023-02-28 18:03:18 +0000337 )
Skyler Greyda16adf2023-03-05 10:22:12 +0000338 .map((message) => message as Message);
339 const newOut = await client.database.transcripts.createTranscript(
Skyler Greye0c511b2023-03-06 10:30:17 +0000340 "purge",
Skyler Greyda16adf2023-03-05 10:22:12 +0000341 messageArray,
342 interaction,
343 interaction.member as GuildMember
344 );
pineafande618c42023-02-28 18:03:18 +0000345
TheCodedProf75c51be2023-03-03 17:18:18 -0500346 const [code, key, iv] = await client.database.transcripts.create(newOut);
Samuel Shuertcc63dee2023-03-03 18:54:29 -0500347
pineafande618c42023-02-28 18:03:18 +0000348 await interaction.editReply({
PineaFan1dee28f2023-01-16 22:09:07 +0000349 embeds: [
350 new EmojiEmbed()
351 .setEmoji("CHANNEL.PURGE.GREEN")
352 .setTitle("Purge")
353 .setDescription("Messages cleared")
354 .setStatus("Success")
355 ],
356 components: [
357 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
Skyler Greyda16adf2023-03-05 10:22:12 +0000358 new ButtonBuilder()
359 .setLabel("View")
360 .setStyle(ButtonStyle.Link)
361 .setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`)
362 .setDisabled(!code)
PineaFan1dee28f2023-01-16 22:09:07 +0000363 ])
364 ]
pineafande618c42023-02-28 18:03:18 +0000365 });
pineafan8b4b17f2022-02-27 20:42:52 +0000366 }
pineafan63fc5e22022-08-04 22:04:10 +0100367};
pineafan4f164f32022-02-26 22:07:12 +0000368
TheCodedProff86ba092023-01-27 17:10:07 -0500369const check = (interaction: CommandInteraction, partial: boolean = false) => {
PineaFana34d04b2023-01-03 22:05:42 +0000370 if (!interaction.guild) return false;
Skyler Grey75ea9172022-08-06 10:22:23 +0100371 const member = interaction.member as GuildMember;
TheCodedProff86ba092023-01-27 17:10:07 -0500372 // Check if the user has manage_messages permission
373 if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
374 if (partial) return true;
PineaFana34d04b2023-01-03 22:05:42 +0000375 const me = interaction.guild.members.me!;
pineafanc1c18792022-08-03 21:41:36 +0100376 // Check if nucleus has the manage_messages permission
PineaFan0d06edc2023-01-17 22:10:31 +0000377 if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000378 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100379 if (member.id === interaction.guild.ownerId) return true;
pineafanc1c18792022-08-03 21:41:36 +0100380 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100381 return true;
382};
pineafan4f164f32022-02-26 22:07:12 +0000383
Skyler Grey75ea9172022-08-06 10:22:23 +0100384export { command, callback, check };
TheCodedProfa112f612023-01-28 18:06:45 -0500385export const metadata = {
Skyler Greyda16adf2023-03-05 10:22:12 +0000386 longDescription:
387 "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.",
388 premiumOnly: true
389};