blob: 02d5d2a2467238acfa3bcebd2fd5661342aa99ae [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';
pineafan96228bd2023-02-21 14:22:55 +00004import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, Message, MessageContextMenuCommandInteraction } from "discord.js";
PineaFana34d04b2023-01-03 22:05:42 +00005import client from "../../utils/client.js";
6import getEmojiByName from '../../utils/getEmojiByName.js';
PineaFan0d06edc2023-01-17 22:10:31 +00007import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from "../../utils/logTranscripts.js";
TheCodedProf94ff6de2023-02-22 17:47:26 -05008import { messageException } from '../../utils/createTemporaryStorage.js';
PineaFana34d04b2023-01-03 22:05:42 +00009
10const command = new ContextMenuCommandBuilder()
11 .setName("Purge up to here")
12
13
14async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise<boolean> {
15 let component;
16 try {
TheCodedProf267563a2023-01-21 17:00:57 -050017 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 +000018 } catch (e) {
19 return false;
20 }
21 (await component).deferUpdate();
22 return true;
23}
24
25
26const callback = async (interaction: MessageContextMenuCommandInteraction) => {
27 await interaction.targetMessage.fetch();
28 const targetMessage = interaction.targetMessage;
29 const targetMember: Discord.User = targetMessage.author;
30 let allowedMessage: Discord.Message | undefined = undefined;
31 const channel = interaction.channel;
32 if (!channel) return;
33 await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
34 // Option for "include this message"?
35 // Option for "Only selected user"?
36
37 const history: Discord.Collection<string, Discord.Message> = await channel.messages.fetch({ limit: 100 });
38 if (Date.now() - targetMessage.createdTimestamp > 2 * 7 * 24 * 60 * 60 * 1000) {
39 const m = await interaction.editReply({ embeds: [new EmojiEmbed()
40 .setTitle("Purge")
41 .setDescription("The message you selected is older than 2 weeks. Discord only allows bots to delete messages that are 2 weeks old or younger.")
42 .setEmoji("CHANNEL.PURGE.RED")
43 .setStatus("Danger")
44 ], components: [
45 new ActionRowBuilder<ButtonBuilder>().addComponents(
46 new ButtonBuilder()
47 .setCustomId("oldest")
48 .setLabel("Select first allowed message")
49 .setStyle(ButtonStyle.Primary),
50 )
51 ]});
52 if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
53 } else if (!history.has(targetMessage.id)) {
54 const m = await interaction.editReply({ embeds: [new EmojiEmbed()
55 .setTitle("Purge")
56 .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.")
57 .setEmoji("CHANNEL.PURGE.YELLOW")
58 .setStatus("Warning")
59 ], components: [
60 new ActionRowBuilder<ButtonBuilder>().addComponents(
61 new ButtonBuilder()
62 .setCustomId("oldest")
63 .setLabel("Select first allowed message")
64 .setStyle(ButtonStyle.Primary),
65 )
66 ]});
67 if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
68 } else {
69 allowedMessage = targetMessage;
70 }
71
72 if (!allowedMessage) {
73 // Find the oldest message thats younger than 2 weeks
74 const messages = history.filter(m => Date.now() - m.createdTimestamp < 2 * 7 * 24 * 60 * 60 * 1000);
75 allowedMessage = messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp).first();
76 }
77
78 if (!allowedMessage) {
79 await interaction.editReply({ embeds: [new EmojiEmbed()
80 .setTitle("Purge")
81 .setDescription("There are no valid messages in the last 100 messages. (No messages younger than 2 weeks)")
82 .setEmoji("CHANNEL.PURGE.RED")
83 .setStatus("Danger")
84 ], components: [] });
85 return;
86 }
87
88 let reason: string | null = null
89 let confirmation;
90 let chosen = false;
91 let timedOut = false;
92 let deleteSelected = true;
93 let deleteUser = false;
94 do {
95 confirmation = await new confirmationMessage(interaction)
96 .setEmoji("CHANNEL.PURGE.RED")
97 .setTitle("Purge")
98 .setDescription(
99 `[[Selected Message]](${allowedMessage.url})\n\n` +
100 (reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*") + "\n\n" +
101 `Are you sure you want to delete all messages from below the selected message?`
102 )
103 .addCustomBoolean(
104 "includeSelected",
105 "Include selected message",
106 false,
107 undefined,
108 "The selected message will be deleted as well.",
109 "The selected message will not be deleted.",
110 "CONTROL." + (deleteSelected ? "TICK" : "CROSS"),
111 deleteSelected
112 )
113 .addCustomBoolean(
114 "onlySelectedUser",
115 "Only selected user",
116 false,
117 undefined,
118 `Only messages from <@${targetMember.id}> will be deleted.`,
119 `All messages will be deleted.`,
120 "CONTROL." + (deleteUser ? "TICK" : "CROSS"),
121 deleteUser
122 )
123 .setColor("Danger")
124 .addReasonButton(reason ?? "")
PineaFan0d06edc2023-01-17 22:10:31 +0000125 .setFailedMessage("No changes were made", "Success", "CHANNEL.PURGE.GREEN")
PineaFana34d04b2023-01-03 22:05:42 +0000126 .send(true)
127 reason = reason ?? ""
128 if (confirmation.cancelled) timedOut = true;
129 else if (confirmation.success !== undefined) chosen = true;
130 else if (confirmation.newReason) reason = confirmation.newReason;
131 else if (confirmation.components) {
132 deleteSelected = confirmation.components["includeSelected"]!.active;
133 deleteUser = confirmation.components["onlySelectedUser"]!.active;
134 }
135 } while (!chosen && !timedOut);
PineaFan0d06edc2023-01-17 22:10:31 +0000136 if (timedOut || !confirmation.success) return;
PineaFana34d04b2023-01-03 22:05:42 +0000137 const filteredMessages = history
138 .filter(m => m.createdTimestamp >= allowedMessage!.createdTimestamp) // older than selected
139 .filter(m => deleteUser ? m.author.id === targetMember.id : true) // only selected user
140 .filter(m => deleteSelected ? true : m.id !== allowedMessage!.id) // include selected
141
142 const deleted = await (channel as GuildTextBasedChannel).bulkDelete(filteredMessages, true);
143 if (deleted.size === 0) {
144 return await interaction.editReply({
145 embeds: [
146 new EmojiEmbed()
147 .setEmoji("CHANNEL.PURGE.RED")
148 .setTitle("Purge")
149 .setDescription("No messages were deleted")
150 .setStatus("Danger")
151 ],
152 components: []
153 });
154 }
155 if (deleteUser) {
156 await client.database.history.create(
157 "purge",
158 interaction.guild!.id,
159 targetMember,
160 interaction.user,
161 reason === "" ? "*No reason provided*" : reason,
162 null,
163 null,
164 deleted.size.toString()
165 );
166 }
167 const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
168 const data = {
169 meta: {
170 type: "channelPurge",
171 displayName: "Channel Purged",
172 calculateType: "messageDelete",
173 color: NucleusColors.red,
174 emoji: "PUNISH.BAN.RED",
TheCodedProf6ec331b2023-02-20 12:13:06 -0500175 timestamp: Date.now()
PineaFana34d04b2023-01-03 22:05:42 +0000176 },
177 list: {
178 memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
179 purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
180 channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as Discord.GuildChannel)),
181 messagesCleared: entry(deleted.size.toString(), deleted.size.toString())
182 },
183 hidden: {
184 guild: interaction.guild!.id
185 }
186 };
187 log(data);
pineafan96228bd2023-02-21 14:22:55 +0000188 const messages: Message[] = deleted.map(m => m).filter(m => m instanceof Message).map(m => m as Message);
TheCodedProf94ff6de2023-02-22 17:47:26 -0500189 if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id)
pineafan96228bd2023-02-21 14:22:55 +0000190 const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(messages)!);
PineaFana34d04b2023-01-03 22:05:42 +0000191 const attachmentObject = {
PineaFan0d06edc2023-01-17 22:10:31 +0000192 attachment: Buffer.from(transcript),
PineaFana34d04b2023-01-03 22:05:42 +0000193 name: `purge-${channel.id}-${Date.now()}.txt`,
194 description: "Purge log"
195 };
196 const m = (await interaction.editReply({
197 embeds: [
198 new EmojiEmbed()
199 .setEmoji("CHANNEL.PURGE.GREEN")
200 .setTitle("Purge")
201 .setDescription("Messages cleared")
202 .setStatus("Success")
203 ],
204 components: [
205 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
206 new Discord.ButtonBuilder()
207 .setCustomId("download")
208 .setLabel("Download transcript")
209 .setStyle(ButtonStyle.Success)
210 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
211 ])
212 ]
213 })) as Discord.Message;
214 let component;
215 try {
216 component = await m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -0500217 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
PineaFana34d04b2023-01-03 22:05:42 +0000218 time: 300000
219 });
220 } catch {
221 return;
222 }
223 if (component.customId === "download") {
224 interaction.editReply({
225 embeds: [
226 new EmojiEmbed()
227 .setEmoji("CHANNEL.PURGE.GREEN")
228 .setTitle("Purge")
229 .setDescription("Transcript uploaded above")
230 .setStatus("Success")
231 ],
232 components: [],
233 files: [attachmentObject]
234 });
235 } else {
236 interaction.editReply({
237 embeds: [
238 new EmojiEmbed()
239 .setEmoji("CHANNEL.PURGE.GREEN")
240 .setTitle("Purge")
241 .setDescription("Messages cleared")
242 .setStatus("Success")
243 ],
244 components: []
245 });
246 }
247}
248
249const check = async (_interaction: MessageContextMenuCommandInteraction) => {
250 return true;
251}
252
253export { command, callback, check }