huge changes once again
diff --git a/src/commands/mod/info.ts b/src/commands/mod/info.ts
new file mode 100644
index 0000000..c3a4388
--- /dev/null
+++ b/src/commands/mod/info.ts
@@ -0,0 +1,285 @@
+import { HistorySchema } from '../../utils/database';
+import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton, TextInputComponent } from "discord.js";
+import { SlashCommandSubcommandBuilder, SelectMenuOption } 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"},
+    "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 = () => {
+        let first = Math.round(this.content[0].data.occurredAt.getTime() / 1000)
+        let 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;
+        }
+        let 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; }
+        }
+        let 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"))
+            ])
+        ])
+        let 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) {
+            let 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;
+    let member = (interaction.options.getMember("user")) as Discord.GuildMember;
+    await interaction.reply({embeds: [new EmojiEmbed()
+        .setEmoji("NUCLEUS.LOADING")
+        .setTitle("Downloading data...")
+        .setStatus("Success")
+    ], 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(
+                // @ts-ignore
+                new MessageActionRow().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) {
+                let 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) => {
+    let 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 };
\ No newline at end of file