blob: 1170c337ea9c599d1bfa10ead8a2cef0ddc6d29b [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 Greyf4f21c42023-03-08 14:36:29 +0000103 component = await m.awaitMessageComponent({
pineafan1e462ab2023-03-07 21:34:06 +0000104 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id,
Skyler Grey75ea9172022-08-06 10:22:23 +0100105 time: 300000
106 });
107 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100108 timedOut = true;
109 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100110 }
Skyler Greyf4f21c42023-03-08 14:36:29 +0000111 await component.deferUpdate();
112 if (component.customId === "done") {
Skyler Greyad002172022-08-16 18:48:26 +0100113 amountSelected = true;
114 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100115 }
Skyler Greyf4f21c42023-03-08 14:36:29 +0000116 const amount = parseInt(component.customId);
Skyler Greyad002172022-08-16 18:48:26 +0100117
PineaFana34d04b2023-01-03 22:05:42 +0000118 let messages: Discord.Message[] = [];
Skyler Grey11236ba2022-08-08 21:13:33 +0100119 await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => {
120 if (user) {
121 ms = ms.filter((m) => m.author.id === user.id);
122 }
Skyler Greyda16adf2023-03-05 10:22:12 +0000123 messages = (await (channel as TextChannel).bulkDelete(ms, true)).map((m) => m as Discord.Message);
Skyler Grey11236ba2022-08-08 21:13:33 +0100124 });
PineaFana34d04b2023-01-03 22:05:42 +0000125 deleted = deleted.concat(messages);
pineafan8b4b17f2022-02-27 20:42:52 +0000126 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100127 if (deleted.length === 0)
128 return await interaction.editReply({
129 embeds: [
130 new EmojiEmbed()
131 .setEmoji("CHANNEL.PURGE.RED")
132 .setTitle("Purge")
133 .setDescription("No messages were deleted")
134 .setStatus("Danger")
135 ],
136 components: []
137 });
pineafan4edb7762022-06-26 19:21:04 +0100138 if (user) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100139 await client.database.history.create(
140 "purge",
141 interaction.guild.id,
PineaFana34d04b2023-01-03 22:05:42 +0000142 user.user,
143 interaction.user,
Skyler Greyda16adf2023-03-05 10:22:12 +0000144 (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*",
Skyler Grey75ea9172022-08-06 10:22:23 +0100145 null,
146 null,
PineaFana34d04b2023-01-03 22:05:42 +0000147 deleted.length.toString()
Skyler Grey75ea9172022-08-06 10:22:23 +0100148 );
pineafan4edb7762022-06-26 19:21:04 +0100149 }
Skyler Grey11236ba2022-08-08 21:13:33 +0100150 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
pineafan63fc5e22022-08-04 22:04:10 +0100151 const data = {
152 meta: {
153 type: "channelPurge",
154 displayName: "Channel Purged",
155 calculateType: "messageDelete",
156 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000157 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500158 timestamp: Date.now()
pineafan63fc5e22022-08-04 22:04:10 +0100159 },
160 list: {
Skyler Grey11236ba2022-08-08 21:13:33 +0100161 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
162 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
PineaFana34d04b2023-01-03 22:05:42 +0000163 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
164 messagesCleared: entry(deleted.length.toString(), deleted.length.toString())
pineafan63fc5e22022-08-04 22:04:10 +0100165 },
166 hidden: {
167 guild: interaction.guild.id
pineafane625d782022-05-09 18:04:32 +0100168 }
pineafan63fc5e22022-08-04 22:04:10 +0100169 };
Skyler Greyf4f21c42023-03-08 14:36:29 +0000170 await log(data);
Skyler Greyda16adf2023-03-05 10:22:12 +0000171 const newOut = await client.database.transcripts.createTranscript(
Skyler Greye0c511b2023-03-06 10:30:17 +0000172 "purge",
Skyler Greyda16adf2023-03-05 10:22:12 +0000173 deleted,
174 interaction,
175 interaction.member as GuildMember
176 );
TheCodedProf9c51a7e2023-02-27 17:11:13 -0500177 const transcript = client.database.transcripts.toHumanReadable(newOut);
pineafan63fc5e22022-08-04 22:04:10 +0100178 const attachmentObject = {
PineaFan0d06edc2023-01-17 22:10:31 +0000179 attachment: Buffer.from(transcript),
pineafan63fc5e22022-08-04 22:04:10 +0100180 name: `purge-${channel.id}-${Date.now()}.txt`,
181 description: "Purge log"
182 };
Skyler Grey75ea9172022-08-06 10:22:23 +0100183 const m = (await interaction.editReply({
184 embeds: [
185 new EmojiEmbed()
186 .setEmoji("CHANNEL.PURGE.GREEN")
187 .setTitle("Purge")
188 .setDescription("Messages cleared")
189 .setStatus("Success")
190 ],
191 components: [
PineaFana34d04b2023-01-03 22:05:42 +0000192 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -0400193 new Discord.ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100194 .setCustomId("download")
195 .setLabel("Download transcript")
TheCodedProf21c08592022-09-13 14:14:43 -0400196 .setStyle(ButtonStyle.Success)
Skyler Grey75ea9172022-08-06 10:22:23 +0100197 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
198 ])
199 ]
200 })) as Discord.Message;
pineafan5d1908e2022-02-28 21:34:47 +0000201 let component;
202 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100203 component = await m.awaitMessageComponent({
Skyler Greyda16adf2023-03-05 10:22:12 +0000204 filter: (i) =>
205 i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
Skyler Grey75ea9172022-08-06 10:22:23 +0100206 time: 300000
207 });
208 } catch {
209 return;
210 }
PineaFana34d04b2023-01-03 22:05:42 +0000211 if (component.customId === "download") {
Skyler Greyf4f21c42023-03-08 14:36:29 +0000212 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100213 embeds: [
214 new EmojiEmbed()
215 .setEmoji("CHANNEL.PURGE.GREEN")
216 .setTitle("Purge")
217 .setDescription("Uploaded")
218 .setStatus("Success")
219 ],
220 components: [],
221 files: [attachmentObject]
222 });
pineafan5d1908e2022-02-28 21:34:47 +0000223 } else {
Skyler Greyf4f21c42023-03-08 14:36:29 +0000224 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100225 embeds: [
226 new EmojiEmbed()
227 .setEmoji("CHANNEL.PURGE.GREEN")
228 .setTitle("Purge")
229 .setDescription("Messages cleared")
230 .setStatus("Success")
231 ],
232 components: []
233 });
pineafan5d1908e2022-02-28 21:34:47 +0000234 }
pineafan63fc5e22022-08-04 22:04:10 +0100235 return;
pineafan8b4b17f2022-02-27 20:42:52 +0000236 } else {
pineafan63fc5e22022-08-04 22:04:10 +0100237 const confirmation = await new confirmationMessage(interaction)
pineafan8b4b17f2022-02-27 20:42:52 +0000238 .setEmoji("CHANNEL.PURGE.RED")
239 .setTitle("Purge")
Skyler Grey75ea9172022-08-06 10:22:23 +0100240 .setDescription(
241 keyValueList({
242 channel: `<#${channel.id}>`,
PineaFana34d04b2023-01-03 22:05:42 +0000243 amount: (interaction.options.get("amount")?.value as number).toString(),
Skyler Greyda16adf2023-03-05 10:22:12 +0000244 reason: `\n> ${
245 interaction.options.get("reason")?.value
246 ? interaction.options.get("reason")?.value
247 : "*No reason provided*"
248 }`
Skyler Grey75ea9172022-08-06 10:22:23 +0100249 })
250 )
pineafan8b4b17f2022-02-27 20:42:52 +0000251 .setColor("Danger")
PineaFan1dee28f2023-01-16 22:09:07 +0000252 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
pineafan63fc5e22022-08-04 22:04:10 +0100253 .send();
PineaFan1dee28f2023-01-16 22:09:07 +0000254 if (confirmation.cancelled || !confirmation.success) return;
255 let messages;
256 try {
257 if (!user) {
258 const toDelete = await (interaction.channel as TextChannel).messages.fetch({
259 limit: interaction.options.get("amount")?.value as number
Skyler Grey75ea9172022-08-06 10:22:23 +0100260 });
PineaFan1dee28f2023-01-16 22:09:07 +0000261 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
262 } else {
263 const toDelete = (
Skyler Greyf4f21c42023-03-08 14:36:29 +0000264 await (interaction.channel as TextChannel).messages.fetch({
265 limit: 100
266 })
267 )
268 .filter((m) => m.author.id === user.id)
269 .first(interaction.options.get("amount")?.value as number);
PineaFan1dee28f2023-01-16 22:09:07 +0000270 messages = await (channel as TextChannel).bulkDelete(toDelete, true);
pineafan8b4b17f2022-02-27 20:42:52 +0000271 }
PineaFan1dee28f2023-01-16 22:09:07 +0000272 } catch (e) {
273 await interaction.editReply({
274 embeds: [
275 new EmojiEmbed()
276 .setEmoji("CHANNEL.PURGE.RED")
277 .setTitle("Purge")
278 .setDescription("Something went wrong and no messages were deleted")
279 .setStatus("Danger")
280 ],
281 components: []
pineafan63fc5e22022-08-04 22:04:10 +0100282 });
PineaFan1dee28f2023-01-16 22:09:07 +0000283 }
284 if (!messages) {
285 await interaction.editReply({
286 embeds: [
287 new EmojiEmbed()
288 .setEmoji("CHANNEL.PURGE.RED")
289 .setTitle("Purge")
290 .setDescription("No messages could be deleted")
291 .setStatus("Danger")
292 ],
293 components: []
294 });
295 return;
296 }
297 if (user) {
298 await client.database.history.create(
299 "purge",
300 interaction.guild.id,
301 user.user,
302 interaction.user,
Skyler Greyda16adf2023-03-05 10:22:12 +0000303 (interaction.options.get("reason")?.value as string | null) ?? "*No reason provided*",
PineaFan1dee28f2023-01-16 22:09:07 +0000304 null,
305 null,
306 messages.size.toString()
307 );
308 }
309 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
310 const data = {
311 meta: {
312 type: "channelPurge",
313 displayName: "Channel Purged",
314 calculateType: "messageDelete",
315 color: NucleusColors.red,
PineaFan0d06edc2023-01-17 22:10:31 +0000316 emoji: "CHANNEL.PURGE.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500317 timestamp: Date.now()
PineaFan1dee28f2023-01-16 22:09:07 +0000318 },
319 list: {
320 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
321 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
322 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)),
323 messagesCleared: entry(messages.size.toString(), messages.size.toString())
324 },
325 hidden: {
326 guild: interaction.guild.id
327 }
328 };
Skyler Greyf4f21c42023-03-08 14:36:29 +0000329 await log(data);
Skyler Greyda16adf2023-03-05 10:22:12 +0000330 const messageArray: Message[] = messages
331 .filter(
332 (message) =>
333 !message!.components.some((component) =>
334 component.components.some((child) => child.customId?.includes("transcript") ?? false)
335 )
pineafande618c42023-02-28 18:03:18 +0000336 )
Skyler Greyda16adf2023-03-05 10:22:12 +0000337 .map((message) => message as Message);
338 const newOut = await client.database.transcripts.createTranscript(
Skyler Greye0c511b2023-03-06 10:30:17 +0000339 "purge",
Skyler Greyda16adf2023-03-05 10:22:12 +0000340 messageArray,
341 interaction,
342 interaction.member as GuildMember
343 );
pineafande618c42023-02-28 18:03:18 +0000344
TheCodedProf75c51be2023-03-03 17:18:18 -0500345 const [code, key, iv] = await client.database.transcripts.create(newOut);
Samuel Shuertcc63dee2023-03-03 18:54:29 -0500346
pineafande618c42023-02-28 18:03:18 +0000347 await interaction.editReply({
PineaFan1dee28f2023-01-16 22:09:07 +0000348 embeds: [
349 new EmojiEmbed()
350 .setEmoji("CHANNEL.PURGE.GREEN")
351 .setTitle("Purge")
352 .setDescription("Messages cleared")
353 .setStatus("Success")
354 ],
355 components: [
356 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
Skyler Greyda16adf2023-03-05 10:22:12 +0000357 new ButtonBuilder()
358 .setLabel("View")
359 .setStyle(ButtonStyle.Link)
360 .setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`)
361 .setDisabled(!code)
PineaFan1dee28f2023-01-16 22:09:07 +0000362 ])
363 ]
pineafande618c42023-02-28 18:03:18 +0000364 });
pineafan8b4b17f2022-02-27 20:42:52 +0000365 }
pineafan63fc5e22022-08-04 22:04:10 +0100366};
pineafan4f164f32022-02-26 22:07:12 +0000367
TheCodedProff86ba092023-01-27 17:10:07 -0500368const check = (interaction: CommandInteraction, partial: boolean = false) => {
PineaFana34d04b2023-01-03 22:05:42 +0000369 if (!interaction.guild) return false;
Skyler Grey75ea9172022-08-06 10:22:23 +0100370 const member = interaction.member as GuildMember;
TheCodedProff86ba092023-01-27 17:10:07 -0500371 // Check if the user has manage_messages permission
372 if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
373 if (partial) return true;
PineaFana34d04b2023-01-03 22:05:42 +0000374 const me = interaction.guild.members.me!;
pineafanc1c18792022-08-03 21:41:36 +0100375 // Check if nucleus has the manage_messages permission
PineaFan0d06edc2023-01-17 22:10:31 +0000376 if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
pineafan8b4b17f2022-02-27 20:42:52 +0000377 // Allow the owner to purge
pineafan63fc5e22022-08-04 22:04:10 +0100378 if (member.id === interaction.guild.ownerId) return true;
pineafanc1c18792022-08-03 21:41:36 +0100379 // Allow purge
pineafan63fc5e22022-08-04 22:04:10 +0100380 return true;
381};
pineafan4f164f32022-02-26 22:07:12 +0000382
Skyler Grey75ea9172022-08-06 10:22:23 +0100383export { command, callback, check };
TheCodedProfa112f612023-01-28 18:06:45 -0500384export const metadata = {
Skyler Greyda16adf2023-03-05 10:22:12 +0000385 longDescription:
386 "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.",
387 premiumOnly: true
388};