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