blob: f0257f8ab9bfdd619e0ab57908a516cc306e10ec [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;
106 while (true) {
107 if (refresh) {
Skyler Grey11236ba2022-08-08 21:13:33 +0100108 history = await client.database.history.read(member.guild.id, member.id, currentYear);
pineafan3a02ea32022-08-11 21:35:04 +0100109 history = history
110 .sort(
111 (a: { occurredAt: Date }, b: { occurredAt: Date }) =>
112 b.occurredAt.getTime() - a.occurredAt.getTime()
113 )
114 .reverse();
pineafan4edb7762022-06-26 19:21:04 +0100115 if (openFilterPane) {
pineafan63fc5e22022-08-04 22:04:10 +0100116 let tempFilteredTypes = filteredTypes;
Skyler Grey75ea9172022-08-06 10:22:23 +0100117 if (filteredTypes.length === 0) {
118 tempFilteredTypes = Object.keys(types);
119 }
pineafan3a02ea32022-08-11 21:35:04 +0100120 history = history.filter((h: { type: string }) => tempFilteredTypes.includes(h.type));
pineafan63fc5e22022-08-04 22:04:10 +0100121 }
pineafan4edb7762022-06-26 19:21:04 +0100122 refresh = false;
123 }
pineafan63fc5e22022-08-04 22:04:10 +0100124 const groups: TimelineSection[] = [];
pineafan4edb7762022-06-26 19:21:04 +0100125 if (history.length > 0) {
pineafan63fc5e22022-08-04 22:04:10 +0100126 current = new TimelineSection();
pineafan3a02ea32022-08-11 21:35:04 +0100127 history.forEach((event: HistorySchema) => {
Skyler Grey11236ba2022-08-08 21:13:33 +0100128 if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) {
pineafan4edb7762022-06-26 19:21:04 +0100129 groups.push(current);
130 current.generateName();
131 current = new TimelineSection();
132 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100133 current.addContent({
134 data: event,
135 rendered: historyToString(event)
136 });
pineafan4edb7762022-06-26 19:21:04 +0100137 });
138 current.generateName();
139 groups.push(current);
Skyler Grey75ea9172022-08-06 10:22:23 +0100140 if (pageIndex === null) {
141 pageIndex = groups.length - 1;
142 }
pineafan4edb7762022-06-26 19:21:04 +0100143 }
pineafan3a02ea32022-08-11 21:35:04 +0100144 if (pageIndex === null) pageIndex = 0;
pineafan63fc5e22022-08-04 22:04:10 +0100145 const components = (
Skyler Grey75ea9172022-08-06 10:22:23 +0100146 openFilterPane
147 ? [
148 new MessageActionRow().addComponents([
149 new Discord.MessageSelectMenu()
150 .setOptions(
151 Object.entries(types).map(([key, value]) => ({
152 label: value.text,
153 value: key,
154 default: filteredTypes.includes(key),
Skyler Grey11236ba2022-08-08 21:13:33 +0100155 emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))
Skyler Grey75ea9172022-08-06 10:22:23 +0100156 }))
157 )
158 .setMinValues(1)
159 .setMaxValues(Object.keys(types).length)
160 .setCustomId("filter")
161 .setPlaceholder("Select at least one event")
162 ])
163 ]
164 : []
165 ).concat([
pineafan4edb7762022-06-26 19:21:04 +0100166 new MessageActionRow().addComponents([
167 new MessageButton()
168 .setCustomId("prevYear")
169 .setLabel((currentYear - 1).toString())
170 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
171 .setStyle("SECONDARY"),
Skyler Grey11236ba2022-08-08 21:13:33 +0100172 new MessageButton().setCustomId("prevPage").setLabel("Previous page").setStyle("PRIMARY"),
173 new MessageButton().setCustomId("today").setLabel("Today").setStyle("PRIMARY"),
pineafan4edb7762022-06-26 19:21:04 +0100174 new MessageButton()
175 .setCustomId("nextPage")
176 .setLabel("Next page")
177 .setStyle("PRIMARY")
Skyler Grey11236ba2022-08-08 21:13:33 +0100178 .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
pineafan4edb7762022-06-26 19:21:04 +0100179 new MessageButton()
180 .setCustomId("nextYear")
181 .setLabel((currentYear + 1).toString())
182 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
183 .setStyle("SECONDARY")
pineafan63fc5e22022-08-04 22:04:10 +0100184 .setDisabled(currentYear === new Date().getFullYear())
Skyler Grey75ea9172022-08-06 10:22:23 +0100185 ]),
186 new MessageActionRow().addComponents([
pineafan4edb7762022-06-26 19:21:04 +0100187 new MessageButton()
188 .setLabel("Mod notes")
189 .setCustomId("modNotes")
190 .setStyle("PRIMARY")
191 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
192 new MessageButton()
193 .setLabel("Filter")
194 .setCustomId("openFilter")
195 .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY")
196 .setEmoji(getEmojiByName("ICONS.FILTER", "id"))
197 ])
pineafan63fc5e22022-08-04 22:04:10 +0100198 ]);
Skyler Grey75ea9172022-08-06 10:22:23 +0100199 const end =
200 "\n\nJanuary " +
201 currentYear.toString() +
Skyler Grey11236ba2022-08-08 21:13:33 +0100202 pageIndicator(Math.max(groups.length, 1), groups.length === 0 ? 1 : pageIndex) +
203 (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December") +
Skyler Grey75ea9172022-08-06 10:22:23 +0100204 " " +
205 currentYear.toString();
pineafan4edb7762022-06-26 19:21:04 +0100206 if (groups.length > 0) {
pineafan3a02ea32022-08-11 21:35:04 +0100207 const toRender = groups[Math.min(pageIndex, groups.length - 1)]!;
208 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100209 embeds: [
210 new EmojiEmbed()
211 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100212 .setTitle("Moderation history for " + member.user.username)
Skyler Grey75ea9172022-08-06 10:22:23 +0100213 .setDescription(
Skyler Grey11236ba2022-08-08 21:13:33 +0100214 `**${toRender.name}**\n\n` + toRender.content.map((c) => c.rendered).join("\n") + end
Skyler Grey75ea9172022-08-06 10:22:23 +0100215 )
216 .setStatus("Success")
217 .setFooter({
Skyler Grey11236ba2022-08-08 21:13:33 +0100218 text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : ""
Skyler Grey75ea9172022-08-06 10:22:23 +0100219 })
220 ],
221 components: components
pineafan3a02ea32022-08-11 21:35:04 +0100222 })) as Message;
pineafan4edb7762022-06-26 19:21:04 +0100223 } else {
pineafan3a02ea32022-08-11 21:35:04 +0100224 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100225 embeds: [
226 new EmojiEmbed()
227 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100228 .setTitle("Moderation history for " + member.user.username)
229 .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end)
Skyler Grey75ea9172022-08-06 10:22:23 +0100230 .setStatus("Success")
231 .setFooter({
Skyler Grey11236ba2022-08-08 21:13:33 +0100232 text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : ""
Skyler Grey75ea9172022-08-06 10:22:23 +0100233 })
234 ],
235 components: components
pineafan3a02ea32022-08-11 21:35:04 +0100236 })) as Message;
pineafan4edb7762022-06-26 19:21:04 +0100237 }
pineafan3a02ea32022-08-11 21:35:04 +0100238 let i: MessageComponentInteraction;
pineafan4edb7762022-06-26 19:21:04 +0100239 try {
240 i = await m.awaitMessageComponent({ time: 300000 });
241 } catch (e) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100242 interaction.editReply({
243 embeds: [
244 new EmojiEmbed()
245 .setEmoji("MEMBER.JOIN")
Skyler Grey11236ba2022-08-08 21:13:33 +0100246 .setTitle("Moderation history for " + member.user.username)
pineafan3a02ea32022-08-11 21:35:04 +0100247 .setDescription(m.embeds[0]!.description!)
Skyler Grey75ea9172022-08-06 10:22:23 +0100248 .setStatus("Danger")
249 .setFooter({ text: "Message timed out" })
250 ]
251 });
pineafan63fc5e22022-08-04 22:04:10 +0100252 return 0;
pineafan4edb7762022-06-26 19:21:04 +0100253 }
pineafan63fc5e22022-08-04 22:04:10 +0100254 i.deferUpdate();
pineafan4edb7762022-06-26 19:21:04 +0100255 if (i.customId === "filter") {
256 filteredTypes = i.values;
257 pageIndex = null;
258 refresh = true;
259 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100260 if (i.customId === "prevYear") {
261 currentYear--;
262 pageIndex = null;
263 refresh = true;
264 }
265 if (i.customId === "nextYear") {
266 currentYear++;
267 pageIndex = null;
268 refresh = true;
269 }
pineafan4edb7762022-06-26 19:21:04 +0100270 if (i.customId === "prevPage") {
pineafan3a02ea32022-08-11 21:35:04 +0100271 pageIndex!--;
272 if (pageIndex! < 0) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100273 pageIndex = null;
274 currentYear--;
275 refresh = true;
276 }
pineafan4edb7762022-06-26 19:21:04 +0100277 }
278 if (i.customId === "nextPage") {
pineafan3a02ea32022-08-11 21:35:04 +0100279 pageIndex!++;
280 if (pageIndex! >= groups.length) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100281 pageIndex = 0;
282 currentYear++;
283 refresh = true;
284 }
pineafan4edb7762022-06-26 19:21:04 +0100285 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100286 if (i.customId === "today") {
287 currentYear = new Date().getFullYear();
288 pageIndex = null;
289 refresh = true;
290 }
291 if (i.customId === "modNotes") {
292 return 1;
293 }
294 if (i.customId === "openFilter") {
295 openFilterPane = !openFilterPane;
296 refresh = true;
297 }
pineafan4edb7762022-06-26 19:21:04 +0100298 }
299}
300
pineafan3a02ea32022-08-11 21:35:04 +0100301const callback = async (interaction: CommandInteraction): Promise<unknown> => {
302 let m: Message;
Skyler Grey75ea9172022-08-06 10:22:23 +0100303 const member = interaction.options.getMember("user") as Discord.GuildMember;
304 await interaction.reply({
Skyler Grey11236ba2022-08-08 21:13:33 +0100305 embeds: [new EmojiEmbed().setEmoji("NUCLEUS.LOADING").setTitle("Downloading Data").setStatus("Danger")],
Skyler Grey75ea9172022-08-06 10:22:23 +0100306 ephemeral: true,
307 fetchReply: true
308 });
pineafan4edb7762022-06-26 19:21:04 +0100309 let note;
310 let firstLoad = true;
311 while (true) {
312 note = await client.database.notes.read(member.guild.id, member.id);
Skyler Grey75ea9172022-08-06 10:22:23 +0100313 if (firstLoad && !note) {
314 await showHistory(member, interaction);
315 }
pineafan4edb7762022-06-26 19:21:04 +0100316 firstLoad = false;
pineafan3a02ea32022-08-11 21:35:04 +0100317 m = (await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100318 embeds: [
319 new EmojiEmbed()
320 .setEmoji("MEMBER.JOIN")
321 .setTitle("Mod notes for " + member.user.username)
322 .setDescription(note ? note : "*No note set*")
323 .setStatus("Success")
324 ],
325 components: [
326 new MessageActionRow().addComponents([
327 new MessageButton()
328 .setLabel(`${note ? "Modify" : "Create"} note`)
329 .setStyle("PRIMARY")
330 .setCustomId("modify")
331 .setEmoji(getEmojiByName("ICONS.EDIT", "id")),
332 new MessageButton()
333 .setLabel("View moderation history")
334 .setStyle("PRIMARY")
335 .setCustomId("history")
336 .setEmoji(getEmojiByName("ICONS.HISTORY", "id"))
337 ])
338 ]
pineafan3a02ea32022-08-11 21:35:04 +0100339 })) as Message;
340 let i: MessageComponentInteraction;
pineafan4edb7762022-06-26 19:21:04 +0100341 try {
342 i = await m.awaitMessageComponent({ time: 300000 });
Skyler Grey75ea9172022-08-06 10:22:23 +0100343 } catch (e) {
344 return;
345 }
pineafan4edb7762022-06-26 19:21:04 +0100346 if (i.customId === "modify") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100347 await i.showModal(
348 new Discord.Modal()
349 .setCustomId("modal")
350 .setTitle("Editing moderator note")
351 .addComponents(
352 new MessageActionRow<TextInputComponent>().addComponents(
353 new TextInputComponent()
354 .setCustomId("note")
355 .setLabel("Note")
356 .setMaxLength(4000)
357 .setRequired(false)
358 .setStyle("PARAGRAPH")
359 .setValue(note ? note : "")
360 )
361 )
362 );
pineafan4edb7762022-06-26 19:21:04 +0100363 await interaction.editReply({
Skyler Grey75ea9172022-08-06 10:22:23 +0100364 embeds: [
365 new EmojiEmbed()
366 .setTitle("Mod notes for " + member.user.username)
Skyler Grey11236ba2022-08-08 21:13:33 +0100367 .setDescription("Modal opened. If you can't see it, click back and try again.")
Skyler Grey75ea9172022-08-06 10:22:23 +0100368 .setStatus("Success")
369 .setEmoji("GUILD.TICKET.OPEN")
370 ],
371 components: [
372 new MessageActionRow().addComponents([
373 new MessageButton()
374 .setLabel("Back")
375 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
376 .setStyle("PRIMARY")
377 .setCustomId("back")
378 ])
379 ]
pineafan4edb7762022-06-26 19:21:04 +0100380 });
381 let out;
382 try {
Skyler Grey75ea9172022-08-06 10:22:23 +0100383 out = await modalInteractionCollector(
384 m,
pineafan3a02ea32022-08-11 21:35:04 +0100385 (m: Interaction) =>
386 (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId,
Skyler Grey75ea9172022-08-06 10:22:23 +0100387 (m) => m.customId === "modify"
388 );
389 } catch (e) {
pineafan3a02ea32022-08-11 21:35:04 +0100390 break;
Skyler Grey75ea9172022-08-06 10:22:23 +0100391 }
pineafan3a02ea32022-08-11 21:35:04 +0100392 if (out === null) {
393 continue;
394 } else if (out instanceof ModalSubmitInteraction) {
pineafan63fc5e22022-08-04 22:04:10 +0100395 const toAdd = out.fields.getTextInputValue("note") || null;
Skyler Grey11236ba2022-08-08 21:13:33 +0100396 await client.database.notes.create(member.guild.id, member.id, toAdd);
Skyler Grey75ea9172022-08-06 10:22:23 +0100397 } else {
398 continue;
399 }
pineafan4edb7762022-06-26 19:21:04 +0100400 } else if (i.customId === "history") {
401 i.deferUpdate();
Skyler Grey75ea9172022-08-06 10:22:23 +0100402 if (!(await showHistory(member, interaction))) return;
pineafan4edb7762022-06-26 19:21:04 +0100403 }
404 }
pineafan63fc5e22022-08-04 22:04:10 +0100405};
pineafan4edb7762022-06-26 19:21:04 +0100406
pineafanbd02b4a2022-08-05 22:01:38 +0100407const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100408 const member = interaction.member as GuildMember;
pineafan3a02ea32022-08-11 21:35:04 +0100409 if (!member.permissions.has("MODERATE_MEMBERS"))
410 throw new Error("You do not have the *Moderate Members* permission");
pineafan63fc5e22022-08-04 22:04:10 +0100411 return true;
412};
pineafan4edb7762022-06-26 19:21:04 +0100413
414export { command };
415export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100416export { check };