blob: e2ec6e4e9d81354bc0c4c1d057c655d5a28d86eb [file] [log] [blame]
PineaFana34d04b2023-01-03 22:05:42 +00001import confirmationMessage from '../../utils/confirmationMessage.js';
2import EmojiEmbed from '../../utils/generateEmojiEmbed.js';
3import { LoadingEmbed } from './../../utils/defaultEmbeds.js';
4import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js";
5import client from "../../utils/client.js";
6import getEmojiByName from '../../utils/getEmojiByName.js';
7
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 {
15 component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id });
16 } 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 ?? "")
123 .send(true)
124 reason = reason ?? ""
125 if (confirmation.cancelled) timedOut = true;
126 else if (confirmation.success !== undefined) chosen = true;
127 else if (confirmation.newReason) reason = confirmation.newReason;
128 else if (confirmation.components) {
129 deleteSelected = confirmation.components["includeSelected"]!.active;
130 deleteUser = confirmation.components["onlySelectedUser"]!.active;
131 }
132 } while (!chosen && !timedOut);
133 if (timedOut) return;
134 if (!confirmation.success) {
135 await interaction.editReply({ embeds: [new EmojiEmbed()
136 .setTitle("Purge")
137 .setDescription("No changes were made")
138 .setEmoji("CHANNEL.PURGE.GREEN")
139 .setStatus("Success")
140 ], components: [] });
141 return;
142 }
143 const filteredMessages = history
144 .filter(m => m.createdTimestamp >= allowedMessage!.createdTimestamp) // older than selected
145 .filter(m => deleteUser ? m.author.id === targetMember.id : true) // only selected user
146 .filter(m => deleteSelected ? true : m.id !== allowedMessage!.id) // include selected
147
148 const deleted = await (channel as GuildTextBasedChannel).bulkDelete(filteredMessages, true);
149 if (deleted.size === 0) {
150 return await interaction.editReply({
151 embeds: [
152 new EmojiEmbed()
153 .setEmoji("CHANNEL.PURGE.RED")
154 .setTitle("Purge")
155 .setDescription("No messages were deleted")
156 .setStatus("Danger")
157 ],
158 components: []
159 });
160 }
161 if (deleteUser) {
162 await client.database.history.create(
163 "purge",
164 interaction.guild!.id,
165 targetMember,
166 interaction.user,
167 reason === "" ? "*No reason provided*" : reason,
168 null,
169 null,
170 deleted.size.toString()
171 );
172 }
173 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
174 const data = {
175 meta: {
176 type: "channelPurge",
177 displayName: "Channel Purged",
178 calculateType: "messageDelete",
179 color: NucleusColors.red,
180 emoji: "PUNISH.BAN.RED",
181 timestamp: new Date().getTime()
182 },
183 list: {
184 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
185 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
186 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as Discord.GuildChannel)),
187 messagesCleared: entry(deleted.size.toString(), deleted.size.toString())
188 },
189 hidden: {
190 guild: interaction.guild!.id
191 }
192 };
193 log(data);
194 let out = "";
195 deleted.reverse().forEach((message) => {
196 if (!message) {
197 out += "Unknown message\n\n"
198 } else {
199 const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" };
200 out += `${author.username}#${author.discriminator} (${author.id}) [${new Date(
201 message.createdTimestamp
202 ).toISOString()}]\n`;
203 if (message.content) {
204 const lines = message.content.split("\n");
205 lines.forEach((line) => {
206 out += `> ${line}\n`;
207 });
208 }
209 if (message.attachments.size > 0) {
210 message.attachments.forEach((attachment) => {
211 out += `Attachment > ${attachment.url}\n`;
212 });
213 }
214 out += "\n\n";
215 }
216 });
217 const attachmentObject = {
218 attachment: Buffer.from(out),
219 name: `purge-${channel.id}-${Date.now()}.txt`,
220 description: "Purge log"
221 };
222 const m = (await interaction.editReply({
223 embeds: [
224 new EmojiEmbed()
225 .setEmoji("CHANNEL.PURGE.GREEN")
226 .setTitle("Purge")
227 .setDescription("Messages cleared")
228 .setStatus("Success")
229 ],
230 components: [
231 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
232 new Discord.ButtonBuilder()
233 .setCustomId("download")
234 .setLabel("Download transcript")
235 .setStyle(ButtonStyle.Success)
236 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
237 ])
238 ]
239 })) as Discord.Message;
240 let component;
241 try {
242 component = await m.awaitMessageComponent({
243 filter: (m) => m.user.id === interaction.user.id,
244 time: 300000
245 });
246 } catch {
247 return;
248 }
249 if (component.customId === "download") {
250 interaction.editReply({
251 embeds: [
252 new EmojiEmbed()
253 .setEmoji("CHANNEL.PURGE.GREEN")
254 .setTitle("Purge")
255 .setDescription("Transcript uploaded above")
256 .setStatus("Success")
257 ],
258 components: [],
259 files: [attachmentObject]
260 });
261 } else {
262 interaction.editReply({
263 embeds: [
264 new EmojiEmbed()
265 .setEmoji("CHANNEL.PURGE.GREEN")
266 .setTitle("Purge")
267 .setDescription("Messages cleared")
268 .setStatus("Success")
269 ],
270 components: []
271 });
272 }
273}
274
275const check = async (_interaction: MessageContextMenuCommandInteraction) => {
276 return true;
277}
278
279export { command, callback, check }