blob: 3328d64515eb51925ce8ca5795d1e25d4a8a4866 [file] [log] [blame]
import { LoadingEmbed } from "../../utils/defaults.js";
import type { HistorySchema, FlagColors } from "../../utils/database.js";
import Discord, {
CommandInteraction,
GuildMember,
Message,
ActionRowBuilder,
ButtonBuilder,
MessageComponentInteraction,
ButtonStyle,
TextInputStyle,
APIMessageComponentEmoji,
SlashCommandSubcommandBuilder,
StringSelectMenuBuilder,
StringSelectMenuOptionBuilder,
ContextMenuCommandInteraction
} from "discord.js";
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("about")
// .setNameLocalizations({"ru": "info", "zh-CN": "history", "zh-TW": "notes", "pt-BR": "flags"})
.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()
.setMinValues(1)
.setMaxValues(Object.keys(types).length)
.setCustomId("filter")
.setPlaceholder("Select events to show")
.setOptions(
...Object.entries(types).map(([key, value]) =>
new Discord.StringSelectMenuOptionBuilder()
.setLabel(value.text)
.setValue(key)
.setDefault(filteredTypes.includes(key))
.setEmoji(getEmojiByName(value.emoji, "id") as APIMessageComponentEmoji)
)
)
)
]);
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,
filter: (i) => {
return (
i.user.id === interaction.user.id &&
i.channel!.id === interaction.channel!.id &&
i.message.id === m.id
);
}
});
} catch (e) {
await 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;
}
await i.deferUpdate();
if (i.customId === "filter" && i.isStringSelectMenu()) {
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") {
showHistorySelected = true;
}
if (i.customId === "openFilter") {
openFilterPane = !openFilterPane;
refresh = true;
}
}
return timedOut ? 0 : 1;
}
export const noteMenu = async (
member: GuildMember,
interaction: CommandInteraction | ContextMenuCommandInteraction
): Promise<unknown> => {
let m: Message;
await interaction.reply({
embeds: LoadingEmbed,
ephemeral: true,
fetchReply: true
});
let note;
let timedOut = false;
while (!timedOut) {
note = await client.database.notes.read(member.guild.id, member.id);
const colors: Record<string, Discord.ColorResolvable> = {
none: "#424242",
red: "#F27878",
yellow: "#EDC575",
green: "#68D49E",
blue: "#6576CC",
purple: "#D46899",
gray: "#C4C4C4"
};
m = (await interaction.editReply({
embeds: [
new EmojiEmbed()
.setEmoji(`ICONS.FLAGS.${(note?.flag ?? "none").toUpperCase()}`)
.setTitle("Mod notes for " + member.user.username)
.setDescription(note?.note ? note.note : "*No note set*")
.setColor(colors[note?.flag ?? "none"]!)
],
components: [
new ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
new ButtonBuilder()
.setLabel(`${note?.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"))
]),
new ActionRowBuilder<Discord.StringSelectMenuBuilder>().addComponents([
new StringSelectMenuBuilder()
.setCustomId("flag")
.setPlaceholder("Select a flag")
.addOptions(
new StringSelectMenuOptionBuilder()
.setLabel("None")
.setDefault(!note?.flag)
.setValue("none")
.setDescription("Clear")
.setEmoji(getEmojiByName("ICONS.FLAGS.NONE", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Red")
.setDefault(note?.flag === "red")
.setValue("red")
.setEmoji(getEmojiByName("ICONS.FLAGS.RED", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Yellow")
.setDefault(note?.flag === "yellow")
.setValue("yellow")
.setEmoji(getEmojiByName("ICONS.FLAGS.YELLOW", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Green")
.setDefault(note?.flag === "green")
.setValue("green")
.setEmoji(getEmojiByName("ICONS.FLAGS.GREEN", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Blue")
.setDefault(note?.flag === "blue")
.setValue("blue")
.setEmoji(getEmojiByName("ICONS.FLAGS.BLUE", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Purple")
.setDefault(note?.flag === "purple")
.setValue("purple")
.setEmoji(getEmojiByName("ICONS.FLAGS.PURPLE", "id")),
new StringSelectMenuOptionBuilder()
.setLabel("Gray")
.setDefault(note?.flag === "gray")
.setValue("gray")
.setEmoji(getEmojiByName("ICONS.FLAGS.GRAY", "id"))
)
])
]
})) as Message;
let i: MessageComponentInteraction;
try {
i = await m.awaitMessageComponent({
time: 300000,
filter: (i) => {
return (
i.user.id === interaction.user.id &&
i.channel!.id === interaction.channel!.id &&
i.message.id === m.id
);
}
});
} catch (e) {
timedOut = true;
continue;
}
if (i.isButton()) {
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 ? 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, interaction.user);
} catch (e) {
timedOut = true;
continue;
}
if (out === null || out.isButton()) {
continue;
} else {
let toAdd = out.fields.getTextInputValue("note").trim() || null;
if (toAdd === "") toAdd = null;
await client.database.notes.create(member.guild.id, member.id, toAdd);
}
} else if (i.customId === "history") {
await i.deferUpdate();
if (!(await showHistory(member, interaction))) return;
}
} else if (i.isStringSelectMenu()) {
await i.deferUpdate();
let flag: string | null = i.values[0]!;
if (flag === "none") flag = null;
await client.database.notes.flag(member.guild.id, member.id, flag as FlagColors | null);
}
}
};
const callback = async (interaction: CommandInteraction): Promise<void> => {
const member = interaction.options.getMember("user") as Discord.GuildMember;
await noteMenu(member, interaction);
};
const check = (interaction: CommandInteraction) => {
const member = interaction.member as GuildMember;
if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
return true;
};
export { command, callback, check };
export const metadata = {
longDescription:
"Shows the moderation history (all previous bans, kicks, warns etc.), and moderator notes for a user.",
premiumOnly: true
};