Working on viewas
diff --git a/src/commands/mod/about.ts b/src/commands/mod/about.ts
new file mode 100644
index 0000000..c69f4a9
--- /dev/null
+++ b/src/commands/mod/about.ts
@@ -0,0 +1,435 @@
+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,
+} 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("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))
+ // @ts-expect-error
+ .setEmoji(getEmojiByName(value.emoji, "id")) // FIXME: This gives a type error but is valid
+ )))
+ ]);
+ 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 };