More commands fixed and purge to here
diff --git a/src/context/messages/purgeto.ts b/src/context/messages/purgeto.ts
new file mode 100644
index 0000000..e2ec6e4
--- /dev/null
+++ b/src/context/messages/purgeto.ts
@@ -0,0 +1,279 @@
+import confirmationMessage from '../../utils/confirmationMessage.js';
+import EmojiEmbed from '../../utils/generateEmojiEmbed.js';
+import { LoadingEmbed } from './../../utils/defaultEmbeds.js';
+import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js";
+import client from "../../utils/client.js";
+import getEmojiByName from '../../utils/getEmojiByName.js';
+
+const command = new ContextMenuCommandBuilder()
+    .setName("Purge up to here")
+
+
+async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise<boolean> {
+    let component;
+    try {
+        component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id });
+    } catch (e) {
+        return false;
+    }
+    (await component).deferUpdate();
+    return true;
+}
+
+
+const callback = async (interaction: MessageContextMenuCommandInteraction) => {
+    await interaction.targetMessage.fetch();
+    const targetMessage = interaction.targetMessage;
+    const targetMember: Discord.User = targetMessage.author;
+    let allowedMessage: Discord.Message | undefined = undefined;
+    const channel = interaction.channel;
+    if (!channel) return;
+    await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
+    // Option for "include this message"?
+    // Option for "Only selected user"?
+
+    const history: Discord.Collection<string, Discord.Message> = await channel.messages.fetch({ limit: 100 });
+    if (Date.now() - targetMessage.createdTimestamp > 2 * 7 * 24 * 60 * 60 * 1000) {
+        const m = await interaction.editReply({ embeds: [new EmojiEmbed()
+            .setTitle("Purge")
+            .setDescription("The message you selected is older than 2 weeks. Discord only allows bots to delete messages that are 2 weeks old or younger.")
+            .setEmoji("CHANNEL.PURGE.RED")
+            .setStatus("Danger")
+        ], components: [
+            new ActionRowBuilder<ButtonBuilder>().addComponents(
+                new ButtonBuilder()
+                    .setCustomId("oldest")
+                    .setLabel("Select first allowed message")
+                    .setStyle(ButtonStyle.Primary),
+            )
+        ]});
+        if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
+    } else if (!history.has(targetMessage.id)) {
+        const m = await interaction.editReply({ embeds: [new EmojiEmbed()
+            .setTitle("Purge")
+            .setDescription("The message you selected is not in the last 100 messages in this channel. Discord only allows bots to delete 100 messages at a time.")
+            .setEmoji("CHANNEL.PURGE.YELLOW")
+            .setStatus("Warning")
+        ], components: [
+            new ActionRowBuilder<ButtonBuilder>().addComponents(
+                new ButtonBuilder()
+                    .setCustomId("oldest")
+                    .setLabel("Select first allowed message")
+                    .setStyle(ButtonStyle.Primary),
+            )
+        ]});
+        if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return;
+    } else {
+        allowedMessage = targetMessage;
+    }
+
+    if (!allowedMessage) {
+        // Find the oldest message thats younger than 2 weeks
+        const messages = history.filter(m => Date.now() - m.createdTimestamp < 2 * 7 * 24 * 60 * 60 * 1000);
+        allowedMessage = messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp).first();
+    }
+
+    if (!allowedMessage) {
+        await interaction.editReply({ embeds: [new EmojiEmbed()
+            .setTitle("Purge")
+            .setDescription("There are no valid messages in the last 100 messages. (No messages younger than 2 weeks)")
+            .setEmoji("CHANNEL.PURGE.RED")
+            .setStatus("Danger")
+        ], components: [] });
+        return;
+    }
+
+    let reason: string | null = null
+    let confirmation;
+    let chosen = false;
+    let timedOut = false;
+    let deleteSelected = true;
+    let deleteUser = false;
+    do {
+        confirmation = await new confirmationMessage(interaction)
+            .setEmoji("CHANNEL.PURGE.RED")
+            .setTitle("Purge")
+            .setDescription(
+                `[[Selected Message]](${allowedMessage.url})\n\n` +
+                (reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*") + "\n\n" +
+                `Are you sure you want to delete all messages from below the selected message?`
+            )
+            .addCustomBoolean(
+                "includeSelected",
+                "Include selected message",
+                false,
+                undefined,
+                "The selected message will be deleted as well.",
+                "The selected message will not be deleted.",
+                "CONTROL." + (deleteSelected ? "TICK" : "CROSS"),
+                deleteSelected
+            )
+            .addCustomBoolean(
+                "onlySelectedUser",
+                "Only selected user",
+                false,
+                undefined,
+                `Only messages from <@${targetMember.id}> will be deleted.`,
+                `All messages will be deleted.`,
+                "CONTROL." + (deleteUser ? "TICK" : "CROSS"),
+                deleteUser
+            )
+            .setColor("Danger")
+            .addReasonButton(reason ?? "")
+            .send(true)
+        reason = reason ?? ""
+        if (confirmation.cancelled) timedOut = true;
+        else if (confirmation.success !== undefined) chosen = true;
+        else if (confirmation.newReason) reason = confirmation.newReason;
+        else if (confirmation.components) {
+            deleteSelected = confirmation.components["includeSelected"]!.active;
+            deleteUser = confirmation.components["onlySelectedUser"]!.active;
+        }
+    } while (!chosen && !timedOut);
+    if (timedOut) return;
+    if (!confirmation.success) {
+        await interaction.editReply({ embeds: [new EmojiEmbed()
+            .setTitle("Purge")
+            .setDescription("No changes were made")
+            .setEmoji("CHANNEL.PURGE.GREEN")
+            .setStatus("Success")
+        ], components: [] });
+        return;
+    }
+    const filteredMessages = history
+        .filter(m => m.createdTimestamp >= allowedMessage!.createdTimestamp)  // older than selected
+        .filter(m => deleteUser ? m.author.id === targetMember.id : true)  // only selected user
+        .filter(m => deleteSelected ? true : m.id !== allowedMessage!.id)  // include selected
+
+    const deleted = await (channel as GuildTextBasedChannel).bulkDelete(filteredMessages, true);
+    if (deleted.size === 0) {
+        return await interaction.editReply({
+            embeds: [
+                new EmojiEmbed()
+                    .setEmoji("CHANNEL.PURGE.RED")
+                    .setTitle("Purge")
+                    .setDescription("No messages were deleted")
+                    .setStatus("Danger")
+            ],
+            components: []
+        });
+    }
+    if (deleteUser) {
+        await client.database.history.create(
+            "purge",
+            interaction.guild!.id,
+            targetMember,
+            interaction.user,
+            reason === "" ? "*No reason provided*" : reason,
+            null,
+            null,
+            deleted.size.toString()
+        );
+    }
+    const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
+    const data = {
+        meta: {
+            type: "channelPurge",
+            displayName: "Channel Purged",
+            calculateType: "messageDelete",
+            color: NucleusColors.red,
+            emoji: "PUNISH.BAN.RED",
+            timestamp: new Date().getTime()
+        },
+        list: {
+            memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
+            purgedBy: entry(interaction.user.id, renderUser(interaction.user)),
+            channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as Discord.GuildChannel)),
+            messagesCleared: entry(deleted.size.toString(), deleted.size.toString())
+        },
+        hidden: {
+            guild: interaction.guild!.id
+        }
+    };
+    log(data);
+    let out = "";
+    deleted.reverse().forEach((message) => {
+        if (!message) {
+            out += "Unknown message\n\n"
+        } else {
+            const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" };
+            out += `${author.username}#${author.discriminator} (${author.id}) [${new Date(
+                message.createdTimestamp
+            ).toISOString()}]\n`;
+            if (message.content) {
+                const lines = message.content.split("\n");
+                lines.forEach((line) => {
+                    out += `> ${line}\n`;
+                });
+            }
+            if (message.attachments.size > 0) {
+                message.attachments.forEach((attachment) => {
+                    out += `Attachment > ${attachment.url}\n`;
+                });
+            }
+            out += "\n\n";
+        }
+    });
+    const attachmentObject = {
+        attachment: Buffer.from(out),
+        name: `purge-${channel.id}-${Date.now()}.txt`,
+        description: "Purge log"
+    };
+    const m = (await interaction.editReply({
+        embeds: [
+            new EmojiEmbed()
+                .setEmoji("CHANNEL.PURGE.GREEN")
+                .setTitle("Purge")
+                .setDescription("Messages cleared")
+                .setStatus("Success")
+        ],
+        components: [
+            new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
+                new Discord.ButtonBuilder()
+                    .setCustomId("download")
+                    .setLabel("Download transcript")
+                    .setStyle(ButtonStyle.Success)
+                    .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
+            ])
+        ]
+    })) as Discord.Message;
+    let component;
+    try {
+        component = await m.awaitMessageComponent({
+            filter: (m) => m.user.id === interaction.user.id,
+            time: 300000
+        });
+    } catch {
+        return;
+    }
+    if (component.customId === "download") {
+        interaction.editReply({
+            embeds: [
+                new EmojiEmbed()
+                    .setEmoji("CHANNEL.PURGE.GREEN")
+                    .setTitle("Purge")
+                    .setDescription("Transcript uploaded above")
+                    .setStatus("Success")
+            ],
+            components: [],
+            files: [attachmentObject]
+        });
+    } else {
+        interaction.editReply({
+            embeds: [
+                new EmojiEmbed()
+                    .setEmoji("CHANNEL.PURGE.GREEN")
+                    .setTitle("Purge")
+                    .setDescription("Messages cleared")
+                    .setStatus("Success")
+            ],
+            components: []
+        });
+    }
+}
+
+const check = async (_interaction: MessageContextMenuCommandInteraction) => {
+    return true;
+}
+
+export { command, callback, check }