blob: 482af5bdeaa1e848227b19e4432d3c987b806f70 [file] [log] [blame]
import { HistorySchema } from "../../utils/database";
import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton, TextInputComponent } from "discord.js";
import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import { WrappedCheck } from "jshaiku";
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<any> => {
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, defaultCheck: WrappedCheck) => {
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 };