mirror of https://github.com/clickscodes/nucleus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
425 lines
16 KiB
425 lines
16 KiB
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<string, { emoji: string; text: string }> = {
|
|
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 <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: 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<unknown> => {
|
|
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<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: 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 };
|