blob: 3d7e95a42542c8d645e296bd432e0f7e34aeaaa0 [file] [log] [blame]
PineaFana34d04b2023-01-03 22:05:42 +00001import confirmationMessage from '../../utils/confirmationMessage.js';
2import EmojiEmbed from '../../utils/generateEmojiEmbed.js';
PineaFan0d06edc2023-01-17 22:10:31 +00003import { LoadingEmbed } from '../../utils/defaults.js';
pineafande618c42023-02-28 18:03:18 +00004import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildMember, GuildTextBasedChannel, Message, MessageContextMenuCommandInteraction } from "discord.js";
PineaFana34d04b2023-01-03 22:05:42 +00005import client from "../../utils/client.js";
TheCodedProf94ff6de2023-02-22 17:47:26 -05006import { messageException } from '../../utils/createTemporaryStorage.js';
PineaFana34d04b2023-01-03 22:05:42 +00007
8const command = new ContextMenuCommandBuilder()
9 .setName("Purge up to here")
10
11
12async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise<boolean> {
13 let component;
14 try {
TheCodedProf267563a2023-01-21 17:00:57 -050015 component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id && i.channel!.id === m.channel.id && i.message.id === m.id });
PineaFana34d04b2023-01-03 22:05:42 +000016 } catch (e) {
17 return false;
18 }
19 (await component).deferUpdate();
20 return true;
21}
22
23
24const callback = async (interaction: MessageContextMenuCommandInteraction) => {
25 await interaction.targetMessage.fetch();
26 const targetMessage = interaction.targetMessage;
27 const targetMember: Discord.User = targetMessage.author;
28 let allowedMessage: Discord.Message | undefined = undefined;
29 const channel = interaction.channel;
30 if (!channel) return;
31 await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
32 // Option for "include this message"?
33 // Option for "Only selected user"?
34
35 const history: Discord.Collection<string, Discord.Message> = await channel.messages.fetch({ limit: 100 });
36 if (Date.now() - targetMessage.createdTimestamp > 2 * 7 * 24 * 60 * 60 * 1000) {
37 const m = await interaction.editReply({ embeds: [new EmojiEmbed()
38 .setTitle("Purge")
39 .setDescription("The message you selected is older than 2 weeks. Discord only allows bots to delete messages that are 2 weeks old or younger.")
40 .setEmoji("CHANNEL.PURGE.RED")
41 .setStatus("Danger")
42 ], components: [
43 new ActionRowBuilder<ButtonBuilder>().addComponents(
44 new ButtonBuilder()
45 .setCustomId("oldest")
46 .setLabel("Select first allowed message")
47 .setStyle(ButtonStyle.Primary),
48 )
49 ]});
50 if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
51 } else if (!history.has(targetMessage.id)) {
52 const m = await interaction.editReply({ embeds: [new EmojiEmbed()
53 .setTitle("Purge")
54 .setDescription("The message you selected is not in the last 100 messages in this channel. Discord only allows bots to delete 100 messages at a time.")
55 .setEmoji("CHANNEL.PURGE.YELLOW")
56 .setStatus("Warning")
57 ], components: [
58 new ActionRowBuilder<ButtonBuilder>().addComponents(
59 new ButtonBuilder()
60 .setCustomId("oldest")
61 .setLabel("Select first allowed message")
62 .setStyle(ButtonStyle.Primary),
63 )
64 ]});
65 if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
66 } else {
67 allowedMessage = targetMessage;
68 }
69
70 if (!allowedMessage) {
71 // Find the oldest message thats younger than 2 weeks
72 const messages = history.filter(m => Date.now() - m.createdTimestamp < 2 * 7 * 24 * 60 * 60 * 1000);
73 allowedMessage = messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp).first();
74 }
75
76 if (!allowedMessage) {
77 await interaction.editReply({ embeds: [new EmojiEmbed()
78 .setTitle("Purge")
79 .setDescription("There are no valid messages in the last 100 messages. (No messages younger than 2 weeks)")
80 .setEmoji("CHANNEL.PURGE.RED")
81 .setStatus("Danger")
82 ], components: [] });
83 return;
84 }
85
86 let reason: string | null = null
87 let confirmation;
88 let chosen = false;
89 let timedOut = false;
90 let deleteSelected = true;
91 let deleteUser = false;
92 do {
93 confirmation = await new confirmationMessage(interaction)
94 .setEmoji("CHANNEL.PURGE.RED")
95 .setTitle("Purge")
96 .setDescription(
97 `[[Selected Message]](${allowedMessage.url})\n\n` +
98 (reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*") + "\n\n" +
99 `Are you sure you want to delete all messages from below the selected message?`
100 )
101 .addCustomBoolean(
102 "includeSelected",
103 "Include selected message",
104 false,
105 undefined,
106 "The selected message will be deleted as well.",
107 "The selected message will not be deleted.",
108 "CONTROL." + (deleteSelected ? "TICK" : "CROSS"),
109 deleteSelected
110 )
111 .addCustomBoolean(
112 "onlySelectedUser",
113 "Only selected user",
114 false,
115 undefined,
116 `Only messages from <@${targetMember.id}> will be deleted.`,
117 `All messages will be deleted.`,
118 "CONTROL." + (deleteUser ? "TICK" : "CROSS"),
119 deleteUser
120 )
121 .setColor("Danger")
122 .addReasonButton(reason ?? "")
PineaFan0d06edc2023-01-17 22:10:31 +0000123 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
PineaFana34d04b2023-01-03 22:05:42 +0000124 .send(true)
125 reason = reason ?? ""
126 if (confirmation.cancelled) timedOut = true;
127 else if (confirmation.success !== undefined) chosen = true;
128 else if (confirmation.newReason) reason = confirmation.newReason;
129 else if (confirmation.components) {
130 deleteSelected = confirmation.components["includeSelected"]!.active;
131 deleteUser = confirmation.components["onlySelectedUser"]!.active;
132 }
133 } while (!chosen && !timedOut);
PineaFan0d06edc2023-01-17 22:10:31 +0000134 if (timedOut || !confirmation.success) return;
PineaFana34d04b2023-01-03 22:05:42 +0000135 const filteredMessages = history
136 .filter(m => m.createdTimestamp >= allowedMessage!.createdTimestamp) // older than selected
137 .filter(m => deleteUser ? m.author.id === targetMember.id : true) // only selected user
138 .filter(m => deleteSelected ? true : m.id !== allowedMessage!.id) // include selected
139
140 const deleted = await (channel as GuildTextBasedChannel).bulkDelete(filteredMessages, true);
141 if (deleted.size === 0) {
142 return await interaction.editReply({
143 embeds: [
144 new EmojiEmbed()
145 .setEmoji("CHANNEL.PURGE.RED")
146 .setTitle("Purge")
147 .setDescription("No messages were deleted")
148 .setStatus("Danger")
149 ],
150 components: []
151 });
152 }
153 if (deleteUser) {
154 await client.database.history.create(
155 "purge",
156 interaction.guild!.id,
157 targetMember,
158 interaction.user,
159 reason === "" ? "*No reason provided*" : reason,
160 null,
161 null,
162 deleted.size.toString()
163 );
164 }
165 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
166 const data = {
167 meta: {
168 type: "channelPurge",
169 displayName: "Channel Purged",
170 calculateType: "messageDelete",
171 color: NucleusColors.red,
172 emoji: "PUNISH.BAN.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500173 timestamp: Date.now()
PineaFana34d04b2023-01-03 22:05:42 +0000174 },
175 list: {
176 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
177 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
178 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as Discord.GuildChannel)),
179 messagesCleared: entry(deleted.size.toString(), deleted.size.toString())
180 },
181 hidden: {
182 guild: interaction.guild!.id
183 }
184 };
185 log(data);
pineafan96228bd2023-02-21 14:22:55 +0000186 const messages: Message[] = deleted.map(m => m).filter(m => m instanceof Message).map(m => m as Message);
TheCodedProf94ff6de2023-02-22 17:47:26 -0500187 if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id)
pineafande618c42023-02-28 18:03:18 +0000188 const messageArray: Message[] = messages.filter(message => !(
189 message!.components.some(
190 component => component.components.some(
191 child => child.customId?.includes("transcript") ?? false
192 )
193 )
194 )).map(message => message as Message);
195 const transcript = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember);
Samuel Shuertcc63dee2023-03-03 18:54:29 -0500196
TheCodedProf75c51be2023-03-03 17:18:18 -0500197 const [code, key, iv] = await client.database.transcripts.create(transcript);
Samuel Shuertcc63dee2023-03-03 18:54:29 -0500198
pineafande618c42023-02-28 18:03:18 +0000199 await interaction.editReply({
PineaFana34d04b2023-01-03 22:05:42 +0000200 embeds: [
201 new EmojiEmbed()
202 .setEmoji("CHANNEL.PURGE.GREEN")
203 .setTitle("Purge")
204 .setDescription("Messages cleared")
205 .setStatus("Success")
206 ],
207 components: [
208 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf75c51be2023-03-03 17:18:18 -0500209 new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}?key=${key}&iv=${iv}`).setDisabled(!code),
PineaFana34d04b2023-01-03 22:05:42 +0000210 ])
211 ]
pineafande618c42023-02-28 18:03:18 +0000212 });
PineaFana34d04b2023-01-03 22:05:42 +0000213}
214
215const check = async (_interaction: MessageContextMenuCommandInteraction) => {
216 return true;
217}
218
219export { command, callback, check }