| import { HistorySchema } from "../../utils/database"; |
| import Discord, { |
| CommandInteraction, |
| GuildMember, |
| MessageActionRow, |
| MessageButton, |
| TextInputComponent |
| } from "discord.js"; |
| import { 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 = { |
| 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) { |
| let s = `${getEmojiByName(types[history.type].emoji)} ${ |
| history.amount ? history.amount + " " : "" |
| }${types[history.type].text} on <t:${Math.round( |
| history.occurredAt.getTime() / 1000 |
| )}:F>`; |
| 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 = `<t:${first}:F>`); |
| } |
| return (this.name = `<t:${first}:F> - <t:${last}:F>`); |
| }; |
| } |
| |
| const monthNames = [ |
| "January", |
| "February", |
| "March", |
| "April", |
| "May", |
| "June", |
| "July", |
| "August", |
| "September", |
| "October", |
| "November", |
| "December" |
| ]; |
| |
| async function showHistory(member, interaction: CommandInteraction) { |
| let currentYear = new Date().getFullYear(); |
| let pageIndex = null; |
| let m, history, current; |
| let refresh = true; |
| let filteredTypes = []; |
| let openFilterPane = false; |
| while (true) { |
| if (refresh) { |
| history = await client.database.history.read( |
| member.guild.id, |
| member.id, |
| currentYear |
| ); |
| history = history |
| .sort((a, b) => b.occurredAt.getTime() - a.occurredAt.getTime()) |
| .reverse(); |
| if (openFilterPane) { |
| let tempFilteredTypes = filteredTypes; |
| if (filteredTypes.length === 0) { |
| tempFilteredTypes = Object.keys(types); |
| } |
| history = history.filter((h) => |
| tempFilteredTypes.includes(h.type) |
| ); |
| } |
| refresh = false; |
| } |
| const groups: TimelineSection[] = []; |
| if (history.length > 0) { |
| current = new TimelineSection(); |
| history.forEach((event) => { |
| 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; |
| } |
| } |
| 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 |
| }); |
| } 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 |
| }); |
| } |
| let i; |
| 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" }) |
| ] |
| }); |
| return 0; |
| } |
| i.deferUpdate(); |
| if (i.customId === "filter") { |
| filteredTypes = i.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") { |
| return 1; |
| } |
| if (i.customId === "openFilter") { |
| openFilterPane = !openFilterPane; |
| refresh = true; |
| } |
| } |
| } |
| |
| const callback = async ( |
| interaction: CommandInteraction |
| ): Promise<void | unknown> => { |
| let m; |
| 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; |
| while (true) { |
| 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")) |
| ]) |
| ] |
| }); |
| let i; |
| try { |
| i = await m.awaitMessageComponent({ time: 300000 }); |
| } catch (e) { |
| return; |
| } |
| if (i.customId === "modify") { |
| await i.showModal( |
| new Discord.Modal() |
| .setCustomId("modal") |
| .setTitle("Editing moderator note") |
| .addComponents( |
| new MessageActionRow<TextInputComponent>().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) => m.channel.id === interaction.channel.id, |
| (m) => m.customId === "modify" |
| ); |
| } catch (e) { |
| continue; |
| } |
| if (out.fields) { |
| 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 "You do not have the *Moderate Members* permission"; |
| return true; |
| }; |
| |
| export { command }; |
| export { callback }; |
| export { check }; |