import type { HistorySchema } from "../../utils/database.js"; import Discord, { CommandInteraction, GuildMember, Interaction, Message, MessageActionRow, MessageButton, MessageComponentInteraction, ModalSubmitInteraction, SelectMenuInteraction, TextInputComponent } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import client from "../../utils/client.js"; import { modalInteractionCollector } from "../../utils/dualCollector.js"; import pageIndicator from "../../utils/createPageIndicator.js"; const command = (builder: SlashCommandSubcommandBuilder) => builder .setName("info") .setDescription("Shows moderator information about a user") .addUserOption((option) => option.setName("user").setDescription("The user to get information about").setRequired(true) ); const types: Record = { warn: { emoji: "PUNISH.WARN.YELLOW", text: "Warned" }, mute: { emoji: "PUNISH.MUTE.YELLOW", text: "Muted" }, unmute: { emoji: "PUNISH.MUTE.GREEN", text: "Unmuted" }, join: { emoji: "MEMBER.JOIN", text: "Joined" }, leave: { emoji: "MEMBER.LEAVE", text: "Left" }, kick: { emoji: "MEMBER.KICK", text: "Kicked" }, softban: { emoji: "PUNISH.SOFTBAN", text: "Softbanned" }, ban: { emoji: "MEMBER.BAN", text: "Banned" }, unban: { emoji: "MEMBER.UNBAN", text: "Unbanned" }, purge: { emoji: "PUNISH.CLEARHISTORY", text: "Messages cleared" }, nickname: { emoji: "PUNISH.NICKNAME.YELLOW", text: "Nickname changed" } }; function historyToString(history: HistorySchema) { if (!Object.keys(types).includes(history.type)) throw new Error("Invalid history type"); let s = `${getEmojiByName(types[history.type]!.emoji)} ${history.amount ? history.amount + " " : ""}${ types[history.type]!.text } on `; if (history.moderator) { s += ` by <@${history.moderator}>`; } if (history.reason) { s += `\n**Reason:**\n> ${history.reason}`; } if (history.before) { s += `\n**Before:**\n> ${history.before}`; } if (history.after) { s += `\n**After:**\n> ${history.after}`; } return s + "\n"; } class TimelineSection { name: string = ""; content: { data: HistorySchema; rendered: string }[] = []; addContent = (content: { data: HistorySchema; rendered: string }) => { this.content.push(content); return this; }; contentLength = () => { return this.content.reduce((acc, cur) => acc + cur.rendered.length, 0); }; generateName = () => { const first = Math.round(this.content[0]!.data.occurredAt.getTime() / 1000); const last = Math.round(this.content[this.content.length - 1]!.data.occurredAt.getTime() / 1000); if (first === last) { return (this.name = ``); } return (this.name = ` - `); }; } const monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; async function showHistory(member: Discord.GuildMember, interaction: CommandInteraction) { let currentYear = new Date().getFullYear(); let pageIndex: number | null = null; let history, current: TimelineSection; let m: Message; let refresh = true; let filteredTypes: string[] = []; let openFilterPane = false; let timedOut = false; let showHistorySelected = false; while (!timedOut && !showHistorySelected) { if (refresh) { history = await client.database.history.read(member.guild.id, member.id, currentYear); history = history .sort( (a: { occurredAt: Date }, b: { occurredAt: Date }) => b.occurredAt.getTime() - a.occurredAt.getTime() ) .reverse(); if (openFilterPane) { let tempFilteredTypes = filteredTypes; if (filteredTypes.length === 0) { tempFilteredTypes = Object.keys(types); } history = history.filter((h: { type: string }) => tempFilteredTypes.includes(h.type)); } refresh = false; } const groups: TimelineSection[] = []; if (history.length > 0) { current = new TimelineSection(); history.forEach((event: HistorySchema) => { if (current.contentLength() + historyToString(event).length > 2000 || current.content.length === 5) { groups.push(current); current.generateName(); current = new TimelineSection(); } current.addContent({ data: event, rendered: historyToString(event) }); }); current.generateName(); groups.push(current); if (pageIndex === null) { pageIndex = groups.length - 1; } } if (pageIndex === null) pageIndex = 0; const components = ( openFilterPane ? [ new MessageActionRow().addComponents([ new Discord.MessageSelectMenu() .setOptions( Object.entries(types).map(([key, value]) => ({ label: value.text, value: key, default: filteredTypes.includes(key), emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id")) })) ) .setMinValues(1) .setMaxValues(Object.keys(types).length) .setCustomId("filter") .setPlaceholder("Select at least one event") ]) ] : [] ).concat([ new MessageActionRow().addComponents([ new MessageButton() .setCustomId("prevYear") .setLabel((currentYear - 1).toString()) .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) .setStyle("SECONDARY"), new MessageButton().setCustomId("prevPage").setLabel("Previous page").setStyle("PRIMARY"), new MessageButton().setCustomId("today").setLabel("Today").setStyle("PRIMARY"), new MessageButton() .setCustomId("nextPage") .setLabel("Next page") .setStyle("PRIMARY") .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()), new MessageButton() .setCustomId("nextYear") .setLabel((currentYear + 1).toString()) .setEmoji(getEmojiByName("CONTROL.RIGHT", "id")) .setStyle("SECONDARY") .setDisabled(currentYear === new Date().getFullYear()) ]), new MessageActionRow().addComponents([ new MessageButton() .setLabel("Mod notes") .setCustomId("modNotes") .setStyle("PRIMARY") .setEmoji(getEmojiByName("ICONS.EDIT", "id")), new MessageButton() .setLabel("Filter") .setCustomId("openFilter") .setStyle(openFilterPane ? "SUCCESS" : "PRIMARY") .setEmoji(getEmojiByName("ICONS.FILTER", "id")) ]) ]); const end = "\n\nJanuary " + currentYear.toString() + pageIndicator(Math.max(groups.length, 1), groups.length === 0 ? 1 : pageIndex) + (currentYear === new Date().getFullYear() ? monthNames[new Date().getMonth()] : "December") + " " + currentYear.toString(); if (groups.length > 0) { const toRender = groups[Math.min(pageIndex, groups.length - 1)]!; m = (await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") .setTitle("Moderation history for " + member.user.username) .setDescription( `**${toRender.name}**\n\n` + toRender.content.map((c) => c.rendered).join("\n") + end ) .setStatus("Success") .setFooter({ text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "" }) ], components: components })) as Message; } else { m = (await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") .setTitle("Moderation history for " + member.user.username) .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end) .setStatus("Success") .setFooter({ text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "" }) ], components: components })) as Message; } let i: MessageComponentInteraction; try { i = await m.awaitMessageComponent({ time: 300000 }); } catch (e) { interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") .setTitle("Moderation history for " + member.user.username) .setDescription(m.embeds[0]!.description!) .setStatus("Danger") .setFooter({ text: "Message timed out" }) ] }); timedOut = true; continue; } i.deferUpdate(); if (i.customId === "filter") { filteredTypes = (i as SelectMenuInteraction).values; pageIndex = null; refresh = true; } if (i.customId === "prevYear") { currentYear--; pageIndex = null; refresh = true; } if (i.customId === "nextYear") { currentYear++; pageIndex = null; refresh = true; } if (i.customId === "prevPage") { pageIndex!--; if (pageIndex! < 0) { pageIndex = null; currentYear--; refresh = true; } } if (i.customId === "nextPage") { pageIndex!++; if (pageIndex! >= groups.length) { pageIndex = 0; currentYear++; refresh = true; } } if (i.customId === "today") { currentYear = new Date().getFullYear(); pageIndex = null; refresh = true; } if (i.customId === "modNotes") { showHistorySelected = true; } if (i.customId === "openFilter") { openFilterPane = !openFilterPane; refresh = true; } } return timedOut ? 0 : 1; } const callback = async (interaction: CommandInteraction): Promise => { let m: Message; const member = interaction.options.getMember("user") as Discord.GuildMember; await interaction.reply({ embeds: [new EmojiEmbed().setEmoji("NUCLEUS.LOADING").setTitle("Downloading Data").setStatus("Danger")], ephemeral: true, fetchReply: true }); let note; let firstLoad = true; let timedOut = false; while (!timedOut) { note = await client.database.notes.read(member.guild.id, member.id); if (firstLoad && !note) { await showHistory(member, interaction); } firstLoad = false; m = (await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") .setTitle("Mod notes for " + member.user.username) .setDescription(note ? note : "*No note set*") .setStatus("Success") ], components: [ new MessageActionRow().addComponents([ new MessageButton() .setLabel(`${note ? "Modify" : "Create"} note`) .setStyle("PRIMARY") .setCustomId("modify") .setEmoji(getEmojiByName("ICONS.EDIT", "id")), new MessageButton() .setLabel("View moderation history") .setStyle("PRIMARY") .setCustomId("history") .setEmoji(getEmojiByName("ICONS.HISTORY", "id")) ]) ] })) as Message; let i: MessageComponentInteraction; try { i = await m.awaitMessageComponent({ time: 300000 }); } catch (e) { timedOut = true; continue; } if (i.customId === "modify") { await i.showModal( new Discord.Modal() .setCustomId("modal") .setTitle("Editing moderator note") .addComponents( new MessageActionRow().addComponents( new TextInputComponent() .setCustomId("note") .setLabel("Note") .setMaxLength(4000) .setRequired(false) .setStyle("PARAGRAPH") .setValue(note ? note : "") ) ) ); await interaction.editReply({ embeds: [ new EmojiEmbed() .setTitle("Mod notes for " + member.user.username) .setDescription("Modal opened. If you can't see it, click back and try again.") .setStatus("Success") .setEmoji("GUILD.TICKET.OPEN") ], components: [ new MessageActionRow().addComponents([ new MessageButton() .setLabel("Back") .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) .setStyle("PRIMARY") .setCustomId("back") ]) ] }); let out; try { out = await modalInteractionCollector( m, (m: Interaction) => (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId, (m) => m.customId === "modify" ); } catch (e) { timedOut = true; continue; } if (out === null) { continue; } else if (out instanceof ModalSubmitInteraction) { const toAdd = out.fields.getTextInputValue("note") || null; await client.database.notes.create(member.guild.id, member.id, toAdd); } else { continue; } } else if (i.customId === "history") { i.deferUpdate(); if (!(await showHistory(member, interaction))) return; } } }; const check = (interaction: CommandInteraction) => { const member = interaction.member as GuildMember; if (!member.permissions.has("MODERATE_MEMBERS")) throw new Error("You do not have the *Moderate Members* permission"); return true; }; export { command }; export { callback }; export { check };