blob: da2d540c50fedad8a345909eca9f5fb0b0cb4753 [file] [log] [blame]
pineafan3a02ea32022-08-11 21:35:04 +01001import type { HistorySchema } from "../../utils/database.js";
Skyler Grey75ea9172022-08-06 10:22:23 +01002import Discord, {
3 CommandInteraction,
4 GuildMember,
pineafan3a02ea32022-08-11 21:35:04 +01005 Interaction,
6 Message,
Skyler Grey75ea9172022-08-06 10:22:23 +01007 MessageActionRow,
8 MessageButton,
pineafan3a02ea32022-08-11 21:35:04 +01009 MessageComponentInteraction,
10 ModalSubmitInteraction,
Skyler Grey75ea9172022-08-06 10:22:23 +010011 TextInputComponent
12} from "discord.js";
pineafan3a02ea32022-08-11 21:35:04 +010013import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan4edb7762022-06-26 19:21:04 +010014import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
15import getEmojiByName from "../../utils/getEmojiByName.js";
16import client from "../../utils/client.js";
pineafan63fc5e22022-08-04 22:04:10 +010017import { modalInteractionCollector } from "../../utils/dualCollector.js";
18import pageIndicator from "../../utils/createPageIndicator.js";
pineafan4edb7762022-06-26 19:21:04 +010019
20const command = (builder: SlashCommandSubcommandBuilder) =>
21 builder
pineafan63fc5e22022-08-04 22:04:10 +010022 .setName("info")
23 .setDescription("Shows moderator information about a user")
Skyler Grey75ea9172022-08-06 10:22:23 +010024 .addUserOption((option) =>
Skyler Grey11236ba2022-08-08 21:13:33 +010025 option.setName("user").setDescription("The user to get information about").setRequired(true)
Skyler Grey75ea9172022-08-06 10:22:23 +010026 );
pineafan4edb7762022-06-26 19:21:04 +010027
pineafan3a02ea32022-08-11 21:35:04 +010028const types: Record<string, { emoji: string; text: string }> = {
Skyler Grey75ea9172022-08-06 10:22:23 +010029 warn: { emoji: "PUNISH.WARN.YELLOW", text: "Warned" },
30 mute: { emoji: "PUNISH.MUTE.YELLOW", text: "Muted" },
31 unmute: { emoji: "PUNISH.MUTE.GREEN", text: "Unmuted" },
32 join: { emoji: "MEMBER.JOIN", text: "Joined" },
33 leave: { emoji: "MEMBER.LEAVE", text: "Left" },
34 kick: { emoji: "MEMBER.KICK", text: "Kicked" },
35 softban: { emoji: "PUNISH.SOFTBAN", text: "Softbanned" },
36 ban: { emoji: "MEMBER.BAN", text: "Banned" },
37 unban: { emoji: "MEMBER.UNBAN", text: "Unbanned" },
38 purge: { emoji: "PUNISH.CLEARHISTORY", text: "Messages cleared" },
39 nickname: { emoji: "PUNISH.NICKNAME.YELLOW", text: "Nickname changed" }
pineafan63fc5e22022-08-04 22:04:10 +010040};
pineafan4edb7762022-06-26 19:21:04 +010041
42function historyToString(history: HistorySchema) {
pineafan3a02ea32022-08-11 21:35:04 +010043 if (!Object.keys(types).includes(history.type)) throw new Error("Invalid history type");
44 let s = `${getEmojiByName(types[history.type]!.emoji)} ${history.amount ? history.amount + " " : ""}${
45 types[history.type]!.text
Skyler Grey11236ba2022-08-08 21:13:33 +010046 } on <t:${Math.round(history.occurredAt.getTime() / 1000)}:F>`;
Skyler Grey75ea9172022-08-06 10:22:23 +010047 if (history.moderator) {
48 s += ` by <@${history.moderator}>`;
49 }
50 if (history.reason) {
51 s += `\n**Reason:**\n> ${history.reason}`;
52 }
53 if (history.before) {
54 s += `\n**Before:**\n> ${history.before}`;
55 }
56 if (history.after) {
57 s += `\n**After:**\n> ${history.after}`;
58 }
pineafan4edb7762022-06-26 19:21:04 +010059 return s + "\n";
60}
61
pineafan4edb7762022-06-26 19:21:04 +010062class TimelineSection {
pineafan3a02ea32022-08-11 21:35:04 +010063 name: string = "";
Skyler Grey75ea9172022-08-06 10:22:23 +010064 content: { data: HistorySchema; rendered: string }[] = [];
pineafan4edb7762022-06-26 19:21:04 +010065
Skyler Grey75ea9172022-08-06 10:22:23 +010066 addContent = (content: { data: HistorySchema; rendered: string }) => {
67 this.content.push(content);
68 return this;
69 };
70 contentLength = () => {
71 return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0);
72 };
pineafan4edb7762022-06-26 19:21:04 +010073 generateName = () => {
pineafan3a02ea32022-08-11 21:35:04 +010074 const first = Math.round(this.content[0]!.data.occurredAt.getTime() / 1000);
75 const last = Math.round(this.content[this.content.length - 1]!.data.occurredAt.getTime() / 1000);
Skyler Grey75ea9172022-08-06 10:22:23 +010076 if (first === last) {
77 return (this.name = `<t:${first}:F>`);
78 }
79 return (this.name = `<t:${first}:F> - <t:${last}:F>`);
pineafan63fc5e22022-08-04 22:04:10 +010080 };
pineafan4edb7762022-06-26 19:21:04 +010081}
82
Skyler Grey75ea9172022-08-06 10:22:23 +010083const monthNames = [
84 "January",
85 "February",
86 "March",
87 "April",
88 "May",
89 "June",
90 "July",
91 "August",
92 "September",
93 "October",
94 "November",
95 "December"
96];
pineafan4edb7762022-06-26 19:21:04 +010097
pineafan3a02ea32022-08-11 21:35:04 +010098async function showHistory(member: Discord.GuildMember, interaction: CommandInteraction) {
pineafan4edb7762022-06-26 19:21:04 +010099 let currentYear = new Date().getFullYear();
pineafan3a02ea32022-08-11 21:35:04 +0100100 let pageIndex: number | null = null;
101 let history, current: TimelineSection;
102 let m: Message;
pineafan4edb7762022-06-26 19:21:04 +0100103 let refresh = true;
pineafan3a02ea32022-08-11 21:35:04 +0100104 let filteredTypes: string[] = [];
pineafan4edb7762022-06-26 19:21:04 +0100105 let openFilterPane = false;
Skyler Greyad002172022-08-16 18:48:26 +0100106 let timedOut = false;
107 let showHistorySelected = false;
108 while (!timedOut && !showHistorySelected) {
pineafan4edb7762022-06-26 19:21:04 +0100109 if (refresh) {
Skyler Grey11236ba2022-08-08 21:13:33 +0100110 history = await client.database.history.read(member.guild.id, member.id, currentYear);
pineafan3a02ea32022-08-11 21:35:04 +0100111 history = history
112 .sort(
113 (a: { occurredAt: Date }, b: { occurredAt: Date }) =>
114 b.occurredAt.getTime() - a.occurredAt.getTime()
115 )
116 .reverse();
pineafan4edb7762022-06-26 19:21:04 +0100117 if (openFilterPane) {
pineafan63fc5e22022-08-04 22:04:10 +0100118 let tempFilteredTypes = filteredTypes;
Skyler Grey75ea9172022-08-06 10:22:23 +0100119 if (filteredTypes.length === 0) {
120 tempFilteredTypes = Object.keys(types);
121 }
pineafan3a02ea32022-08-11 21:35:04 +0100122 history = history.filter((h: { type: string }) => tempFilteredTypes.includes(h.type));
pineafan63fc5e22022-08-04 22:04:10 +0100123 }
pineafan4edb7762022-06-26 19:21:04 +0100124 refresh = false;
125 }
pineafan63fc5e22022-08-04 22:04:10 +0100126 const groups: TimelineSection[] = [];
pineafan4edb7762022-06-26 19:21:04 +0100127 if (history.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +0100128 current = new TimelineSection();
pineafan3a02ea32022-08-11 21:35:04 +0100129 history.forEach((event: HistorySchema) => {
Skyler Grey11236ba2022-08-08 21:13:33 +0100130 if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) {
pineafan4edb7762022-06-26 19:21:04 +0100131 groups.push(current);
132 current.generateName();
133 current = new TimelineSection();
134 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100135 current.addContent({
136 data: event,
137 rendered: historyToString(event)
138 });
pineafan4edb7762022-06-26 19:21:04 +0100139 });
140 current.generateName();
141 groups.push(current);
Skyler Grey75ea9172022-08-06 10:22:23 +0100142 if (pageIndex === null) {
143 pageIndex = groups.length - 1;
144 }
pineafan4edb7762022-06-26 19:21:04 +0100145 }
pineafan3a02ea32022-08-11 21:35:04 +0100146 if (pageIndex === null) pageIndex = 0;
pineafan63fc5e22022-08-04 22:04:10 +0100147 const components = (
Skyler Grey75ea9172022-08-06 10:22:23 +0100148 openFilterPane
149 ? [
150 new MessageActionRow().addComponents([
151 new Discord.MessageSelectMenu()
152 .setOptions(
153 Object.entries(types).map(([key, value]) => ({
154 label: value.text,
155 value: key,
156 default: filteredTypes.includes(key),
Skyler Grey11236ba2022-08-08 21:13:33 +0100157 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
Skyler Grey75ea9172022-08-06 10:22:23 +0100158 }))
159 )
160 .setMinValues(1)
161 .setMaxValues(Object.keys(types).length)
162 .setCustomId("filter")
163 .setPlaceholder("Select at least one event")
164 ])
165 ]
166 : []
167 ).concat([
pineafan4edb7762022-06-26 19:21:04 +0100168 new MessageActionRow().addComponents([
169 new MessageButton()
170 .setCustomId("prevYear")
171 .setLabel((currentYear - 1).toString())
172 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
173 .setStyle("SECONDARY"),
Skyler Grey11236ba2022-08-08 21:13:33 +0100174 new MessageButton().setCustomId("prevPage").setLabel("Previous page").setStyle("PRIMARY"),
175 new MessageButton().setCustomId("today").setLabel("Today").setStyle("PRIMARY"),
pineafan4edb7762022-06-26 19:21:04 +0100176 new MessageButton()
177 .setCustomId("nextPage")
178 .setLabel("Next page")
179 .setStyle("PRIMARY")
Skyler Grey11236ba2022-08-08 21:13:33 +0100180 .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
pineafan4edb7762022-06-26 19:21:04 +0100181 new MessageButton()
182 .setCustomId("nextYear")
183 .setLabel((currentYear + 1).toString())
184 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
185 .setStyle("SECONDARY")
pineafan63fc5e22022-08-04 22:04:10 +0100186 .setDisabled(currentYear === new Date().getFullYear())
Skyler Grey75ea9172022-08-06 10:22:23 +0100187 ]),
188 new MessageActionRow().addComponents([
pineafan4edb7762022-06-26 19:21:04 +0100189 new MessageButton()
190 .setLabel("Mod notes")
191 .setCustomId("modNotes")
192 .setStyle("PRIMARY")
193 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
194 new MessageButton()
195 .setLabel("Filter")
196 .setCustomId("openFilter")
197 .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY")
198 .setEmoji(getEmojiByName("ICONS.FILTER", "id"))
199 ])
pineafan63fc5e22022-08-04 22:04:10 +0100200 ]);
Skyler Grey75ea9172022-08-06 10:22:23 +0100201 const end =
202 "\n\nJanuary " +
203 currentYear.toString() +
Skyler Grey11236ba2022-08-08 21:13:33 +0100204 pageIndicator(Math.max(groups.length, 1), groups.length === 0 ? 1 : pageIndex) +
205 (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December") +
Skyler Grey75ea9172022-08-06 10:22:23 +0100206 " " +
207 currentYear.toString();
pineafan4edb7762022-06-26 19:21:04 +0100208 if (groups.length > 0) {
pineafan3a02ea32022-08-11 21:35:04 +0100209 const toRender = groups[Math.min(pageIndex, groups.length - 1)]!;
210 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100211 embeds: [
212 new EmojiEmbed()
213 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100214 .setTitle("Moderation history for " + member.user.username)
Skyler Grey75ea9172022-08-06 10:22:23 +0100215 .setDescription(
Skyler Grey11236ba2022-08-08 21:13:33 +0100216 `**${toRender.name}**\n\n` + toRender.content.map((c) => c.rendered).join("\n") + end
Skyler Grey75ea9172022-08-06 10:22:23 +0100217 )
218 .setStatus("Success")
219 .setFooter({
Skyler Grey11236ba2022-08-08 21:13:33 +0100220 text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : ""
Skyler Grey75ea9172022-08-06 10:22:23 +0100221 })
222 ],
223 components: components
pineafan3a02ea32022-08-11 21:35:04 +0100224 })) as Message;
pineafan4edb7762022-06-26 19:21:04 +0100225 } else {
pineafan3a02ea32022-08-11 21:35:04 +0100226 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100227 embeds: [
228 new EmojiEmbed()
229 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100230 .setTitle("Moderation history for " + member.user.username)
231 .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end)
Skyler Grey75ea9172022-08-06 10:22:23 +0100232 .setStatus("Success")
233 .setFooter({
Skyler Grey11236ba2022-08-08 21:13:33 +0100234 text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : ""
Skyler Grey75ea9172022-08-06 10:22:23 +0100235 })
236 ],
237 components: components
pineafan3a02ea32022-08-11 21:35:04 +0100238 })) as Message;
pineafan4edb7762022-06-26 19:21:04 +0100239 }
pineafan3a02ea32022-08-11 21:35:04 +0100240 let i: MessageComponentInteraction;
pineafan4edb7762022-06-26 19:21:04 +0100241 try {
242 i = await m.awaitMessageComponent({ time: 300000 });
243 } catch (e) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100244 interaction.editReply({
245 embeds: [
246 new EmojiEmbed()
247 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100248 .setTitle("Moderation history for " + member.user.username)
pineafan3a02ea32022-08-11 21:35:04 +0100249 .setDescription(m.embeds[0]!.description!)
Skyler Grey75ea9172022-08-06 10:22:23 +0100250 .setStatus("Danger")
251 .setFooter({ text: "Message timed out" })
252 ]
253 });
Skyler Greyad002172022-08-16 18:48:26 +0100254 timedOut = true;
255 continue;
pineafan4edb7762022-06-26 19:21:04 +0100256 }
pineafan63fc5e22022-08-04 22:04:10 +0100257 i.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +0100258 if (i.customId === "filter") {
259 filteredTypes = i.values;
260 pageIndex = null;
261 refresh = true;
262 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100263 if (i.customId === "prevYear") {
264 currentYear--;
265 pageIndex = null;
266 refresh = true;
267 }
268 if (i.customId === "nextYear") {
269 currentYear++;
270 pageIndex = null;
271 refresh = true;
272 }
pineafan4edb7762022-06-26 19:21:04 +0100273 if (i.customId === "prevPage") {
pineafan3a02ea32022-08-11 21:35:04 +0100274 pageIndex!--;
275 if (pageIndex! < 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100276 pageIndex = null;
277 currentYear--;
278 refresh = true;
279 }
pineafan4edb7762022-06-26 19:21:04 +0100280 }
281 if (i.customId === "nextPage") {
pineafan3a02ea32022-08-11 21:35:04 +0100282 pageIndex!++;
283 if (pageIndex! >= groups.length) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100284 pageIndex = 0;
285 currentYear++;
286 refresh = true;
287 }
pineafan4edb7762022-06-26 19:21:04 +0100288 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100289 if (i.customId === "today") {
290 currentYear = new Date().getFullYear();
291 pageIndex = null;
292 refresh = true;
293 }
294 if (i.customId === "modNotes") {
Skyler Greyad002172022-08-16 18:48:26 +0100295 showHistorySelected = true;
Skyler Grey75ea9172022-08-06 10:22:23 +0100296 }
297 if (i.customId === "openFilter") {
298 openFilterPane = !openFilterPane;
299 refresh = true;
300 }
pineafan4edb7762022-06-26 19:21:04 +0100301 }
Skyler Greyad002172022-08-16 18:48:26 +0100302 return timedOut ? 0 : 1;
pineafan4edb7762022-06-26 19:21:04 +0100303}
304
pineafan3a02ea32022-08-11 21:35:04 +0100305const callback = async (interaction: CommandInteraction): Promise<unknown> => {
306 let m: Message;
Skyler Grey75ea9172022-08-06 10:22:23 +0100307 const member = interaction.options.getMember("user") as Discord.GuildMember;
308 await interaction.reply({
Skyler Grey11236ba2022-08-08 21:13:33 +0100309 embeds: [new EmojiEmbed().setEmoji("NUCLEUS.LOADING").setTitle("Downloading Data").setStatus("Danger")],
Skyler Grey75ea9172022-08-06 10:22:23 +0100310 ephemeral: true,
311 fetchReply: true
312 });
pineafan4edb7762022-06-26 19:21:04 +0100313 let note;
314 let firstLoad = true;
Skyler Greyad002172022-08-16 18:48:26 +0100315 let timedOut = false;
316 while (!timedOut) {
pineafan4edb7762022-06-26 19:21:04 +0100317 note = await client.database.notes.read(member.guild.id, member.id);
Skyler Grey75ea9172022-08-06 10:22:23 +0100318 if (firstLoad && !note) {
319 await showHistory(member, interaction);
320 }
pineafan4edb7762022-06-26 19:21:04 +0100321 firstLoad = false;
pineafan3a02ea32022-08-11 21:35:04 +0100322 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100323 embeds: [
324 new EmojiEmbed()
325 .setEmoji("MEMBER.JOIN")
326 .setTitle("Mod notes for " + member.user.username)
327 .setDescription(note ? note : "*No note set*")
328 .setStatus("Success")
329 ],
330 components: [
331 new MessageActionRow().addComponents([
332 new MessageButton()
333 .setLabel(`${note ? "Modify" : "Create"} note`)
334 .setStyle("PRIMARY")
335 .setCustomId("modify")
336 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
337 new MessageButton()
338 .setLabel("View moderation history")
339 .setStyle("PRIMARY")
340 .setCustomId("history")
341 .setEmoji(getEmojiByName("ICONS.HISTORY", "id"))
342 ])
343 ]
pineafan3a02ea32022-08-11 21:35:04 +0100344 })) as Message;
345 let i: MessageComponentInteraction;
pineafan4edb7762022-06-26 19:21:04 +0100346 try {
347 i = await m.awaitMessageComponent({ time: 300000 });
Skyler Grey75ea9172022-08-06 10:22:23 +0100348 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100349 timedOut = true;
350 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100351 }
pineafan4edb7762022-06-26 19:21:04 +0100352 if (i.customId === "modify") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100353 await i.showModal(
354 new Discord.Modal()
355 .setCustomId("modal")
356 .setTitle("Editing moderator note")
357 .addComponents(
358 new MessageActionRow<TextInputComponent>().addComponents(
359 new TextInputComponent()
360 .setCustomId("note")
361 .setLabel("Note")
362 .setMaxLength(4000)
363 .setRequired(false)
364 .setStyle("PARAGRAPH")
365 .setValue(note ? note : "")
366 )
367 )
368 );
pineafan4edb7762022-06-26 19:21:04 +0100369 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100370 embeds: [
371 new EmojiEmbed()
372 .setTitle("Mod notes for " + member.user.username)
Skyler Grey11236ba2022-08-08 21:13:33 +0100373 .setDescription("Modal opened. If you can't see it, click back and try again.")
Skyler Grey75ea9172022-08-06 10:22:23 +0100374 .setStatus("Success")
375 .setEmoji("GUILD.TICKET.OPEN")
376 ],
377 components: [
378 new MessageActionRow().addComponents([
379 new MessageButton()
380 .setLabel("Back")
381 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
382 .setStyle("PRIMARY")
383 .setCustomId("back")
384 ])
385 ]
pineafan4edb7762022-06-26 19:21:04 +0100386 });
387 let out;
388 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100389 out = await modalInteractionCollector(
390 m,
pineafan3a02ea32022-08-11 21:35:04 +0100391 (m: Interaction) =>
392 (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId,
Skyler Grey75ea9172022-08-06 10:22:23 +0100393 (m) => m.customId === "modify"
394 );
395 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100396 timedOut = true;
397 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100398 }
pineafan3a02ea32022-08-11 21:35:04 +0100399 if (out === null) {
400 continue;
401 } else if (out instanceof ModalSubmitInteraction) {
pineafan63fc5e22022-08-04 22:04:10 +0100402 const toAdd = out.fields.getTextInputValue("note") || null;
Skyler Grey11236ba2022-08-08 21:13:33 +0100403 await client.database.notes.create(member.guild.id, member.id, toAdd);
Skyler Grey75ea9172022-08-06 10:22:23 +0100404 } else {
405 continue;
406 }
pineafan4edb7762022-06-26 19:21:04 +0100407 } else if (i.customId === "history") {
408 i.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100409 if (!(await showHistory(member, interaction))) return;
pineafan4edb7762022-06-26 19:21:04 +0100410 }
411 }
pineafan63fc5e22022-08-04 22:04:10 +0100412};
pineafan4edb7762022-06-26 19:21:04 +0100413
pineafanbd02b4a2022-08-05 22:01:38 +0100414const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100415 const member = interaction.member as GuildMember;
pineafan3a02ea32022-08-11 21:35:04 +0100416 if (!member.permissions.has("MODERATE_MEMBERS"))
417 throw new Error("You do not have the *Moderate Members* permission");
pineafan63fc5e22022-08-04 22:04:10 +0100418 return true;
419};
pineafan4edb7762022-06-26 19:21:04 +0100420
421export { command };
422export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100423export { check };