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.
440 lines
17 KiB
440 lines
17 KiB
import { LoadingEmbed } from './../../utils/defaultEmbeds.js';
|
|
import type { HistorySchema } from "../../utils/database.js";
|
|
import Discord, {
|
|
CommandInteraction,
|
|
GuildMember,
|
|
Interaction,
|
|
Message,
|
|
ActionRowBuilder,
|
|
ButtonBuilder,
|
|
MessageComponentInteraction,
|
|
ModalSubmitInteraction,
|
|
ButtonStyle,
|
|
StringSelectMenuInteraction,
|
|
TextInputStyle,
|
|
APIMessageComponentEmoji
|
|
} from "discord.js";
|
|
import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } 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;
|
|
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();
|
|
let m: Message;
|
|
let refresh = false;
|
|
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;
|
|
let components: (ActionRowBuilder<Discord.StringSelectMenuBuilder> | ActionRowBuilder<ButtonBuilder>)[] = []
|
|
if (openFilterPane) components = components.concat([
|
|
new ActionRowBuilder<Discord.StringSelectMenuBuilder>().addComponents(
|
|
new Discord.StringSelectMenuBuilder().setOptions(
|
|
// ...Object.entries(types).map(([key, value]) => new StringSelectMenuOptionBuilder()
|
|
// .setLabel(value.text)
|
|
// .setValue(key)
|
|
// .setDefault(filteredTypes.includes(key))
|
|
// .setEmoji(client.emojis.resolve(getEmojiByName(value.emoji, "id"))! as APIMessageComponentEmoji)
|
|
// )
|
|
...Object.entries(types).map(([key, value]) => ({
|
|
label: value.text,
|
|
value: key,
|
|
default: filteredTypes.includes(key),
|
|
emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id"))! as APIMessageComponentEmoji
|
|
}))
|
|
)
|
|
.setMinValues(1)
|
|
.setMaxValues(Object.keys(types).length)
|
|
.setCustomId("filter")
|
|
.setPlaceholder("Select events to show")
|
|
)
|
|
])
|
|
components = components.concat([new ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
|
|
new ButtonBuilder()
|
|
.setCustomId("prevYear")
|
|
.setLabel((currentYear - 1).toString())
|
|
.setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
|
|
.setStyle(ButtonStyle.Secondary),
|
|
new ButtonBuilder()
|
|
.setCustomId("prevPage")
|
|
.setLabel("Previous page")
|
|
.setStyle(ButtonStyle.Primary),
|
|
new ButtonBuilder().setCustomId("today").setLabel("Today").setStyle(ButtonStyle.Primary),
|
|
new ButtonBuilder()
|
|
.setCustomId("nextPage")
|
|
.setLabel("Next page")
|
|
.setStyle(ButtonStyle.Primary)
|
|
.setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()),
|
|
new ButtonBuilder()
|
|
.setCustomId("nextYear")
|
|
.setLabel((currentYear + 1).toString())
|
|
.setEmoji(getEmojiByName("CONTROL.RIGHT", "id"))
|
|
.setStyle(ButtonStyle.Secondary)
|
|
.setDisabled(currentYear === new Date().getFullYear())
|
|
])])
|
|
components = components.concat([new ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
|
|
new ButtonBuilder()
|
|
.setLabel("Mod notes")
|
|
.setCustomId("modNotes")
|
|
.setStyle(ButtonStyle.Primary)
|
|
.setEmoji(getEmojiByName("ICONS.EDIT", "id")),
|
|
new ButtonBuilder()
|
|
.setLabel("Filter")
|
|
.setCustomId("openFilter")
|
|
.setStyle(openFilterPane ? ButtonStyle.Success : ButtonStyle.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" : "No filters selected"
|
|
})
|
|
],
|
|
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*`)
|
|
.setStatus("Success")
|
|
.setFooter({
|
|
text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "No filters selected"
|
|
})
|
|
],
|
|
components: components
|
|
})
|
|
}
|
|
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 StringSelectMenuInteraction).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: LoadingEmbed,
|
|
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 ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
|
|
new ButtonBuilder()
|
|
.setLabel(`${note ? "Modify" : "Create"} note`)
|
|
.setStyle(ButtonStyle.Primary)
|
|
.setCustomId("modify")
|
|
.setEmoji(getEmojiByName("ICONS.EDIT", "id")),
|
|
new ButtonBuilder()
|
|
.setLabel("Moderation history")
|
|
.setStyle(ButtonStyle.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.ModalBuilder()
|
|
.setCustomId("modal")
|
|
.setTitle("Editing moderator note")
|
|
.addComponents(
|
|
new ActionRowBuilder<Discord.TextInputBuilder>().addComponents(
|
|
new Discord.TextInputBuilder()
|
|
.setCustomId("note")
|
|
.setLabel("Note")
|
|
.setMaxLength(4000)
|
|
.setRequired(false)
|
|
.setStyle(TextInputStyle.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 ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
|
|
new ButtonBuilder()
|
|
.setLabel("Back")
|
|
.setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
|
|
.setStyle(ButtonStyle.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) {
|
|
let toAdd = out.fields.getTextInputValue("note") || null;
|
|
if (toAdd === " ") toAdd = null;
|
|
if (toAdd) toAdd = toAdd.trim();
|
|
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("ModerateMembers"))
|
|
throw new Error("You do not have the *Moderate Members* permission");
|
|
return true;
|
|
};
|
|
|
|
export { command };
|
|
export { callback };
|
|
export { check };
|