blob: b0211ecaec81670d98b226b3f181367144b8d5d7 [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';
PineaFana34d04b2023-01-03 22:05:42 +00004import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js";
5import 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);
PineaFan0d06edc2023-01-17 22:10:31 +0000187 const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted.map((m) => m as Discord.Message))!);
PineaFana34d04b2023-01-03 22:05:42 +0000188 const attachmentObject = {
PineaFan0d06edc2023-01-17 22:10:31 +0000189 attachment: Buffer.from(transcript),
PineaFana34d04b2023-01-03 22:05:42 +0000190 name: `purge-${channel.id}-${Date.now()}.txt`,
191 description: "Purge log"
192 };
193 const m = (await interaction.editReply({
194 embeds: [
195 new EmojiEmbed()
196 .setEmoji("CHANNEL.PURGE.GREEN")
197 .setTitle("Purge")
198 .setDescription("Messages cleared")
199 .setStatus("Success")
200 ],
201 components: [
202 new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
203 new Discord.ButtonBuilder()
204 .setCustomId("download")
205 .setLabel("Download transcript")
206 .setStyle(ButtonStyle.Success)
207 .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
208 ])
209 ]
210 })) as Discord.Message;
211 let component;
212 try {
213 component = await m.awaitMessageComponent({
TheCodedProf267563a2023-01-21 17:00:57 -0500214 filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
PineaFana34d04b2023-01-03 22:05:42 +0000215 time: 300000
216 });
217 } catch {
218 return;
219 }
220 if (component.customId === "download") {
221 interaction.editReply({
222 embeds: [
223 new EmojiEmbed()
224 .setEmoji("CHANNEL.PURGE.GREEN")
225 .setTitle("Purge")
226 .setDescription("Transcript uploaded above")
227 .setStatus("Success")
228 ],
229 components: [],
230 files: [attachmentObject]
231 });
232 } else {
233 interaction.editReply({
234 embeds: [
235 new EmojiEmbed()
236 .setEmoji("CHANNEL.PURGE.GREEN")
237 .setTitle("Purge")
238 .setDescription("Messages cleared")
239 .setStatus("Success")
240 ],
241 components: []
242 });
243 }
244}
245
246const check = async (_interaction: MessageContextMenuCommandInteraction) => {
247 return true;
248}
249
250export { command, callback, check }