Development (#11)

We need this NOW.

---------

Co-authored-by: PineaFan <ash@pinea.dev>
Co-authored-by: pineafan <pineapplefanyt@gmail.com>
Co-authored-by: PineappleFan <PineaFan@users.noreply.github.com>
Co-authored-by: Skyler <skyler3665@gmail.com>
diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts
index 0bd5a9f..fde1340 100644
--- a/src/utils/calculate.ts
+++ b/src/utils/calculate.ts
@@ -17,16 +17,14 @@
     "webhookUpdate",
     "guildMemberVerify",
     "autoModeratorDeleted",
-    "nucleusSettingsUpdated",
-    "ticketUpdate"
+    "ticketUpdate",
+    // "nucleusSettingsUpdated"
 ];
 
 const tickets = ["support", "report", "question", "issue", "suggestion", "other"];
 
 const toHexInteger = (permissions: string[], array?: string[]): string => {
-    if (!array) {
-        array = logs;
-    }
+    if (!array) { array = logs; }
     let int = 0n;
 
     for (const perm of permissions) {
diff --git a/src/utils/client.ts b/src/utils/client.ts
index 46d9f92..b1fa31f 100644
--- a/src/utils/client.ts
+++ b/src/utils/client.ts
@@ -1,11 +1,11 @@
-import Discord, { Client, Interaction, AutocompleteInteraction, GatewayIntentBits } from 'discord.js';
+import Discord, { Client, Interaction, AutocompleteInteraction, Collection } from 'discord.js';
 import { Logger } from "../utils/log.js";
 import Memory from "../utils/memory.js";
 import type { VerifySchema } from "../reflex/verify.js";
-import { Guilds, History, ModNotes, Premium, PerformanceTest } from "../utils/database.js";
+import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache, Transcript,  } from "../utils/database.js";
 import EventScheduler from "../utils/eventScheduler.js";
 import type { RoleMenuSchema } from "../actions/roleMenu.js";
-import config from "../config/main.json" assert { type: "json" };
+import config from "../config/main.js";
 
 
 class NucleusClient extends Client {
@@ -22,36 +22,33 @@
         premium: Premium;
         eventScheduler: EventScheduler;
         performanceTest: PerformanceTest;
+        scanCache: ScanCache;
+        transcripts: Transcript
     };
     preloadPage: Record<string, {command: string, argument: string}> = {};  // e.g. { channelID: { command: privacy, page: 3}}
-    commands: Record<string, {
+    commands: Record<string, [{
         command: Discord.SlashCommandBuilder |
                 ((builder: Discord.SlashCommandBuilder) => Discord.SlashCommandBuilder) |
                 Discord.SlashCommandSubcommandBuilder | ((builder: Discord.SlashCommandSubcommandBuilder) => Discord.SlashCommandSubcommandBuilder) | Discord.SlashCommandSubcommandGroupBuilder | ((builder: Discord.SlashCommandSubcommandGroupBuilder) => Discord.SlashCommandSubcommandGroupBuilder),
         callback: (interaction: Interaction) => Promise<void>,
-        check: (interaction: Interaction) => Promise<boolean> | boolean,
+        check: (interaction: Interaction, partial: boolean) => Promise<boolean> | boolean,
         autocomplete: (interaction: AutocompleteInteraction) => Promise<string[]>
-    }> = {};
-
+    } | undefined, {name: string, description: string}]> = {};
+    fetchedCommands = new Collection<string, Discord.ApplicationCommand>();
     constructor(database: typeof NucleusClient.prototype.database) {
-        super({ intents: [
-            GatewayIntentBits.Guilds,
-            GatewayIntentBits.GuildMessages,
-            GatewayIntentBits.MessageContent,
-            GatewayIntentBits.GuildPresences,
-            GatewayIntentBits.GuildMembers
-        ]});
+        super({ intents: 0b1100011011011111111111});
         this.database = database;
     }
 }
-
 const client = new NucleusClient({
-    guilds: await new Guilds().setup(),
+    guilds: await new Guilds(),
     history: new History(),
     notes: new ModNotes(),
     premium: new Premium(),
     eventScheduler: new EventScheduler(),
-    performanceTest: new PerformanceTest()
+    performanceTest: new PerformanceTest(),
+    scanCache: new ScanCache(),
+    transcripts: new Transcript()
 });
 
 export default client;
diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts
index 281be18..33c88b0 100644
--- a/src/utils/commandRegistration/register.ts
+++ b/src/utils/commandRegistration/register.ts
@@ -1,12 +1,12 @@
 import type { CommandInteraction } from 'discord.js';
 import Discord, { Interaction, SlashCommandBuilder, ApplicationCommandType } from 'discord.js';
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
 import client from "../client.js";
 import fs from "fs";
 import EmojiEmbed from '../generateEmojiEmbed.js';
 import getEmojiByName from '../getEmojiByName.js';
 
-const colours = {
+const colors = {
     red: "\x1b[31m",
     green: "\x1b[32m",
     yellow: "\x1b[33m",
@@ -26,23 +26,26 @@
     for (const file of files) {
         const last = i === files.length - 1 ? "└" : "├";
         if (file.isDirectory()) {
-            console.log(`${last}─ ${colours.yellow}Loading subcommands of ${file.name}${colours.none}`)
-            const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`)).command;
-            commands.push(fetched);
+            console.log(`${last}─ ${colors.yellow}Loading subcommands of ${file.name}${colors.none}`)
+            const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`));
+            commands.push(fetched.command);
         } else if (file.name.endsWith(".js")) {
-            console.log(`${last}─ ${colours.yellow}Loading command ${file.name}${colours.none}`)
+            console.log(`${last}─ ${colors.yellow}Loading command ${file.name}${colors.none}`)
             const fetched = (await import(`../../../${config.commandsFolder}/${file.name}`));
             fetched.command.setDMPermission(fetched.allowedInDMs ?? false)
             fetched.command.setNameLocalizations(fetched.nameLocalizations ?? {})
             fetched.command.setDescriptionLocalizations(fetched.descriptionLocalizations ?? {})
-            if (fetched.nameLocalizations || fetched.descriptionLocalizations) console.log("AAAAA")
+            // if (fetched.nameLocalizations || fetched.descriptionLocalizations)
             commands.push(fetched.command);
-            client.commands["commands/" + fetched.command.name] = fetched;
+            client.commands["commands/" + fetched.command.name] = [
+                fetched,
+                {name: fetched.name ?? fetched.command.name, description: fetched.description ?? fetched.command.description}
+            ];
         }
         i++;
-        console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`)
+        console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`)
     }
-    console.log(`${colours.yellow}Loaded ${commands.length} commands, processing...`)
+    console.log(`${colors.yellow}Loaded ${commands.length} commands, processing...`)
     const processed = []
 
     for (const subcommand of commands) {
@@ -53,7 +56,7 @@
         }
     }
 
-    console.log(`${colours.green}Processed ${processed.length} commands`)
+    console.log(`${colors.green}Processed ${processed.length} commands${colors.none}`)
     return processed;
 
 };
@@ -70,15 +73,15 @@
         const last = i === files.length - 1 ? "└" : "├";
         i++;
         try {
-            console.log(`${last}─ ${colours.yellow}Loading event ${file.name}${colours.none}`)
+            console.log(`${last}─ ${colors.yellow}Loading event ${file.name}${colors.none}`)
             const event = (await import(`../../../${config.eventsFolder}/${file.name}`));
 
             client.on(event.event, event.callback.bind(null, client));
 
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`)
         } catch (e) {
             errors++;
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.red}Failed to load ${file.name} [${i} / ${files.length}]${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.red}Failed to load ${file.name} [${i} / ${files.length}]${colors.none}`)
         }
     }
     console.log(`Loaded ${files.length - errors} events (${errors} failed)`)
@@ -101,36 +104,36 @@
         const last = i === totalFiles - 1 ? "└" : "├";
         i++;
         try {
-            console.log(`${last}─ ${colours.yellow}Loading message context menu ${file.name}${colours.none}`)
+            console.log(`${last}─ ${colors.yellow}Loading message context menu ${file.name}${colors.none}`)
             const context = (await import(`../../../${config.messageContextFolder}/${file.name}`));
             context.command.setType(ApplicationCommandType.Message);
             context.command.setDMPermission(context.allowedInDMs ?? false)
             context.command.setNameLocalizations(context.nameLocalizations ?? {})
             commands.push(context.command);
 
-            client.commands["contextCommands/message/" + context.command.name] = context;
+            client.commands["contextCommands/message/" + context.command.name] = [context, {name: context.name ?? context.command.name, description: context.description ?? context.command.description}];
 
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`)
         } catch (e) {
             errors++;
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colors.none}`)
         }
     }
     for (const file of userFiles) {
         const last = i === totalFiles - 1 ? "└" : "├";
         i++;
         try {
-            console.log(`${last}─ ${colours.yellow}Loading user context menu ${file.name}${colours.none}`)
+            console.log(`${last}─ ${colors.yellow}Loading user context menu ${file.name}${colors.none}`)
             const context = (await import(`../../../${config.userContextFolder}/${file.name}`));
             context.command.setType(ApplicationCommandType.User);
             commands.push(context.command);
 
             client.commands["contextCommands/user/" + context.command.name] = context;
 
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`)
         } catch (e) {
             errors++;
-            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+            console.log(`${last.replace("└", " ").replace("├", "│")}  └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colors.none}`)
         }
     }
 
@@ -142,11 +145,11 @@
     client.on("interactionCreate", async (interaction: Interaction) => {
         if (interaction.isUserContextMenuCommand()) {;
             const commandName = "contextCommands/user/" + interaction.commandName;
-            execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction)
+            execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction)
             return;
         } else if (interaction.isMessageContextMenuCommand()) {
             const commandName = "contextCommands/message/" + interaction.commandName;
-            execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction)
+            execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction)
             return;
         } else if (interaction.isAutocomplete()) {
             const commandName = interaction.commandName;
@@ -155,7 +158,7 @@
 
             const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : "");
 
-            const choices = await client.commands[fullCommandName]?.autocomplete(interaction);
+            const choices = await client.commands[fullCommandName]![0]?.autocomplete(interaction);
 
             const formatted = (choices ?? []).map(choice => {
                 return { name: choice, value: choice }
@@ -168,7 +171,8 @@
 
             const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : "");
 
-            const command = client.commands[fullCommandName];
+            // console.log(fullCommandName, client.commands[fullCommandName])
+            const command = client.commands[fullCommandName]![0];
             const callback = command?.callback;
             const check = command?.check;
             execute(check, callback, interaction);
@@ -177,6 +181,7 @@
 }
 
 async function execute(check: Function | undefined, callback: Function | undefined, data: CommandInteraction) {
+    // console.log(client.commands["contextCommands/user/User info"])
     if (!callback) return;
     if (check) {
         let result;
@@ -189,12 +194,11 @@
         if (typeof result === "string") {
             const { NucleusColors } = client.logger
             return data.reply({embeds: [new EmojiEmbed()
-                .setTitle("")
                 .setDescription(result)
                 .setColor(NucleusColors.red)
                 .setEmoji(getEmojiByName("CONTROL.BLOCKCROSS"))
-            ]});
-        };
+            ], ephemeral: true});
+        }
     }
     callback(data);
 }
@@ -207,17 +211,20 @@
     if (process.argv.includes("--update-commands")) {
         if (config.enableDevelopment) {
             const guild = await client.guilds.fetch(config.developmentGuildID);
-            console.log(`${colours.purple}Registering commands in ${guild!.name}${colours.none}`)
+            console.log(`${colors.purple}Registering commands in ${guild!.name}${colors.none}`)
             await guild.commands.set(commandList);
         } else {
-            console.log(`${colours.blue}Registering commands in production mode${colours.none}`)
+            console.log(`${colors.blue}Registering commands in production mode${colors.none}`)
+            const guild = await client.guilds.fetch(config.developmentGuildID);
+            await guild.commands.set([]);
             await client.application?.commands.set(commandList);
         }
     }
     await registerCommandHandler();
     await registerEvents();
-    console.log(`${colours.green}Registered commands, events and context menus${colours.none}`)
+    console.log(`${colors.green}Registered commands, events and context menus${colors.none}`)
     console.log(
-        (config.enableDevelopment ? `${colours.purple}Bot started in Development mode` :
-        `${colours.blue}Bot started in Production mode`) + colours.none)
+        (config.enableDevelopment ? `${colors.purple}Bot started in Development mode` :
+        `${colors.blue}Bot started in Production mode`) + colors.none
+    )
 };
diff --git a/src/utils/commandRegistration/slashCommandBuilder.ts b/src/utils/commandRegistration/slashCommandBuilder.ts
index b2927d6..66291b3 100644
--- a/src/utils/commandRegistration/slashCommandBuilder.ts
+++ b/src/utils/commandRegistration/slashCommandBuilder.ts
@@ -1,12 +1,12 @@
-import type { SlashCommandSubcommandGroupBuilder } from "@discordjs/builders";
+import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from "discord.js";
 import type { SlashCommandBuilder } from "discord.js";
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
 import getSubcommandsInFolder from "./getFilesInFolder.js";
 import client from "../client.js";
 import Discord from "discord.js";
 
 
-const colours = {
+const colors = {
     red: "\x1b[31m",
     green: "\x1b[32m",
     none: "\x1b[0m"
@@ -23,7 +23,7 @@
     // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString
     console.log(`│  ├─ Loading group ${name}`)
     const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path, "│  ")
-    console.log(`│  │  └─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colours.none}`)
+    console.log(`│  │  └─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colors.none}`)
     return (subcommandGroup: SlashCommandSubcommandGroupBuilder) => {
         subcommandGroup
             .setName(name)
@@ -32,7 +32,9 @@
         if (descriptionLocalizations) { subcommandGroup.setDescriptionLocalizations(descriptionLocalizations) }
 
         for (const subcommand of fetched.subcommands) {
-            subcommandGroup.addSubcommand(subcommand.command);
+            const processedCommand = subcommand.command(new SlashCommandSubcommandBuilder());
+            client.commands["commands/" + path + "/" + processedCommand.name] = [subcommand, { name: processedCommand.name, description: processedCommand.description }]
+            subcommandGroup.addSubcommand(processedCommand);
         };
 
         return subcommandGroup;
@@ -52,7 +54,9 @@
     // If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString
     commandString = "commands/" + (commandString ?? path);
     const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path);
-    console.log(`│  ├─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colours.none}`)
+    console.log(`│  ├─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colors.none}`)
+    // console.log({name: name, description: description})
+    client.commands[commandString!] = [undefined, { name: name, description: description }]
     return (command: SlashCommandBuilder) => {
         command.setName(name)
         command.setDescription(description)
@@ -68,15 +72,17 @@
         for (const subcommand of fetched.subcommands) {
             let fetchedCommand;
             if (subcommand.command instanceof Function) {
-                fetchedCommand = subcommand.command(new Discord.SlashCommandSubcommandBuilder());
+                fetchedCommand = subcommand.command(new SlashCommandSubcommandBuilder());
             } else {
                 fetchedCommand = subcommand.command;
             }
-            client.commands[commandString! + "/" + fetchedCommand.name] = subcommand
+            client.commands[commandString! + "/" + fetchedCommand.name] = [subcommand, { name: fetchedCommand.name, description: fetchedCommand.description }]
             command.addSubcommand(fetchedCommand);
         }
         for (const group of fetched.subcommandGroups) {
-            command.addSubcommandGroup(group.command);
+            const processedCommand = group.command(new SlashCommandSubcommandGroupBuilder());
+            client.commands[commandString! + "/" + processedCommand.name] = [undefined, { name: processedCommand.name, description: processedCommand.description }]
+            command.addSubcommandGroup(processedCommand);
         };
         return command;
     };
diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts
index 4d90676..f7cccaf 100644
--- a/src/utils/confirmationMessage.ts
+++ b/src/utils/confirmationMessage.ts
@@ -1,11 +1,9 @@
-import { TextInputBuilder } from "@discordjs/builders";
+import { TextInputBuilder } from "discord.js";
 import Discord, {
     CommandInteraction,
-    Interaction,
     Message,
     ActionRowBuilder,
     ButtonBuilder,
-    MessageComponentInteraction,
     ModalSubmitInteraction,
     ButtonStyle,
     TextInputStyle
@@ -183,13 +181,12 @@
             let component;
             try {
                 component = await m.awaitMessageComponent({
-                    filter: (m) => m.user.id === this.interaction.user.id && m.channel!.id === this.interaction.channel!.id,
+                    filter: (i) => i.user.id === this.interaction.user.id && i.channel!.id === this.interaction.channel!.id,
                     time: 300000
                 });
             } catch (e) {
                 success = false;
-                returnComponents = true;
-                continue;
+                break;
             }
             if (component.customId === "yes") {
                 component.deferUpdate();
@@ -247,17 +244,12 @@
                 });
                 let out;
                 try {
-                    out = await modalInteractionCollector(
-                        m,
-                        (m: Interaction) =>
-                            (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === this.interaction.channelId,
-                        (m) => m.customId === "reason"
-                    );
+                    out = await modalInteractionCollector(m, this.interaction.user) as Discord.ModalSubmitInteraction | null;
                 } catch (e) {
                     cancelled = true;
                     continue;
                 }
-                if (out === null) {
+                if (out === null  || out.isButton()) {
                     cancelled = true;
                     continue;
                 }
@@ -277,23 +269,23 @@
         }
         const returnValue: Awaited<ReturnType<typeof this.send>> = {};
 
-        if (returnComponents || success !== undefined) returnValue.components = this.customButtons;
-        if (success !== undefined) returnValue.success = success;
         if (cancelled) {
             await this.timeoutError()
             returnValue.cancelled = true;
         }
-        if (success == false) {
+        if (success === false) {
             await this.interaction.editReply({
                 embeds: [new EmojiEmbed()
                     .setTitle(this.title)
-                    .setDescription(this.failedMessage ?? "")
+                    .setDescription(this.failedMessage ?? "*Message timed out*")
                     .setStatus(this.failedStatus ?? "Danger")
                     .setEmoji(this.failedEmoji ?? this.redEmoji ?? this.emoji)
                 ], components: []
             });
             return {success: false}
         }
+        if (returnComponents || success !== undefined) returnValue.components = this.customButtons;
+        if (success !== undefined) returnValue.success = success;
         if (newReason) returnValue.newReason = newReason;
 
         const typedReturnValue = returnValue as {cancelled: true} |
diff --git a/src/utils/convertCurlyBracketString.ts b/src/utils/convertCurlyBracketString.ts
index 5d2c23d..058ba16 100644
--- a/src/utils/convertCurlyBracketString.ts
+++ b/src/utils/convertCurlyBracketString.ts
@@ -13,7 +13,7 @@
         .replace("{member:mention}", memberID ? `<@${memberID}>` : "{member:mention}")
         .replace("{member:name}", memberName ? `${memberName}` : "{member:name}")
         .replace("{serverName}", serverName ? `${serverName}` : "{serverName}")
-        .replace("{memberCount}", memberCount ? `${memberCount}` : "{memberCount}")
+        .replace("{memberCount:all}", memberCount ? `${memberCount}` : "{memberCount}")
         .replace("{memberCount:bots}", bots ? `${bots}` : "{memberCount:bots}")
         .replace("{memberCount:humans}", memberCount && bots ? `${memberCount - bots}` : "{memberCount:humans}");
 
diff --git a/src/utils/createPageIndicator.ts b/src/utils/createPageIndicator.ts
index ee123d6..6bc86a4 100644
--- a/src/utils/createPageIndicator.ts
+++ b/src/utils/createPageIndicator.ts
@@ -1,17 +1,17 @@
 import getEmojiByName from "./getEmojiByName.js";
 
-function pageIndicator(amount: number, selected: number, showDetails?: boolean | true) {
+function pageIndicator(amount: number, selected: number, showDetails?: boolean, disabled?: boolean | string) {
     let out = "";
-
+    disabled = disabled ? "GRAY." : ""
     if (amount === 1) {
-        out += getEmojiByName("TRACKS.SINGLE." + (selected === 0 ? "ACTIVE" : "INACTIVE"));
+        out += getEmojiByName("TRACKS.SINGLE." + (disabled) + (selected === 0 ? "ACTIVE" : "INACTIVE"));
     } else {
         for (let i = 0; i < amount; i++) {
             out += getEmojiByName(
                 "TRACKS.HORIZONTAL." +
-                    (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") +
-                    "." +
-                    (i === selected ? "ACTIVE" : "INACTIVE")
+                (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") +
+                "." + (disabled) +
+                (i === selected ? "ACTIVE" : "INACTIVE")
             );
         }
     }
@@ -21,4 +21,23 @@
     return out;
 }
 
+export const verticalTrackIndicator = (position: number, active: string | boolean, size: number, disabled: string | boolean) => {
+    active = active ? "ACTIVE" : "INACTIVE";
+    disabled = disabled ? "GRAY." : "";
+    if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active;
+    if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active;
+    if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active;
+    return "TRACKS.VERTICAL.MIDDLE." + disabled + active;
+};
+
+export const createVerticalTrack = (items: string[], active: boolean[], disabled?: boolean[]) => {
+    let out = "";
+    if (!disabled) disabled = new Array(items.length).fill(false);
+    for (let i = 0; i < items.length; i++) {
+        out += getEmojiByName(verticalTrackIndicator(i, active[i] ?? false, items.length, disabled[i] ?? false));
+        out += items[i] + "\n";
+    }
+    return out;
+}
+
 export default pageIndicator;
diff --git a/src/utils/createTemporaryStorage.ts b/src/utils/createTemporaryStorage.ts
index a684d9d..e8a8073 100644
--- a/src/utils/createTemporaryStorage.ts
+++ b/src/utils/createTemporaryStorage.ts
@@ -1,6 +1,6 @@
 import client from "./client.js";
 
-function generalException(location: string) {
+export function generalException(location: string) {
     client.noLog.push(location);
     setTimeout(() => {
         client.noLog = client.noLog.filter((i: string) => {
@@ -29,4 +29,4 @@
         })
         client.preloadPage = Object.fromEntries(object);
     }, 60 * 5 * 1000);
-}
\ No newline at end of file
+}
diff --git a/src/utils/database.ts b/src/utils/database.ts
index 1e8e990..2e64320 100644
--- a/src/utils/database.ts
+++ b/src/utils/database.ts
@@ -1,32 +1,45 @@
+import { ButtonStyle, CommandInteraction, ComponentType, GuildMember, Message, MessageComponentInteraction } from "discord.js";
 import type Discord from "discord.js";
 import { Collection, MongoClient } from "mongodb";
-import config from "../config/main.json" assert { type: "json" };
-
-const mongoClient = new MongoClient(config.mongoUrl);
+import config from "../config/main.js";
+import client from "../utils/client.js";
+import * as crypto from "crypto";
+import _ from "lodash";
+import defaultData from '../config/default.js';
+// config.mongoOptions.host, {
+//     auth: {
+//         username: config.mongoOptions.username,
+//         password: config.mongoOptions.password
+//     },
+//     authSource: config.mongoOptions.authSource
+// }
+// mongodb://emails:SweetLife2023!!@127.0.0.1:28180/saveEmail?retryWrites=true&w=majority
+const username = encodeURIComponent(config.mongoOptions.username);
+const password = encodeURIComponent(config.mongoOptions.password);
+const mongoClient = new MongoClient(username ? `mongodb://${username}:${password}@${config.mongoOptions.host}` : `mongodb://${config.mongoOptions.host}`, {authSource: "admin"});
 await mongoClient.connect();
-const database = mongoClient.db("Nucleus");
+const database = mongoClient.db();
+
+const collectionOptions = { authdb: "admin" };
 
 export class Guilds {
     guilds: Collection<GuildConfig>;
-    defaultData: GuildConfig | null;
+    defaultData: GuildConfig;
 
     constructor() {
         this.guilds = database.collection<GuildConfig>("guilds");
-        this.defaultData = null;
-    }
-
-    async setup(): Promise<Guilds> {
-        this.defaultData = (await import("../config/default.json", { assert: { type: "json" } }))
-            .default as unknown as GuildConfig;
-        return this;
+        this.defaultData = defaultData;
     }
 
     async read(guild: string): Promise<GuildConfig> {
+        // console.log("Guild read")
         const entry = await this.guilds.findOne({ id: guild });
-        return Object.assign({}, this.defaultData, entry);
+        const data = _.clone(this.defaultData!);
+        return _.merge(data, entry ?? {});
     }
 
     async write(guild: string, set: object | null, unset: string[] | string = []) {
+        // console.log("Guild write")
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const uo: Record<string, any> = {};
         if (!Array.isArray(unset)) unset = [unset];
@@ -41,6 +54,7 @@
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     async append(guild: string, key: string, value: any) {
+        // console.log("Guild append")
         if (Array.isArray(value)) {
             await this.guilds.updateOne(
                 { id: guild },
@@ -67,7 +81,7 @@
         value: any,
         innerKey?: string | null
     ) {
-        console.log(Array.isArray(value));
+        // console.log("Guild remove")
         if (innerKey) {
             await this.guilds.updateOne(
                 { id: guild },
@@ -96,10 +110,255 @@
     }
 
     async delete(guild: string) {
+        // console.log("Guild delete")
         await this.guilds.deleteOne({ id: guild });
     }
 }
 
+interface TranscriptEmbed {
+    title?: string;
+    description?: string;
+    fields?: {
+        name: string;
+        value: string;
+        inline: boolean;
+    }[];
+    footer?: {
+        text: string;
+        iconURL?: string;
+    };
+    color?: number;
+    timestamp?: string;
+    author?: {
+        name: string;
+        iconURL?: string;
+        url?: string;
+    };
+}
+
+interface TranscriptComponent {
+    type: number;
+    style?: ButtonStyle;
+    label?: string;
+    description?: string;
+    placeholder?: string;
+    emojiURL?: string;
+}
+
+interface TranscriptAuthor {
+    username: string;
+    discriminator: number;
+    nickname?: string;
+    id: string;
+    iconURL?: string;
+    topRole: {
+        color: number;
+        badgeURL?: string;
+    };
+    bot: boolean;
+}
+
+interface TranscriptAttachment {
+    url: string;
+    filename: string;
+    size: number;
+    log?: string;
+}
+
+interface TranscriptMessage {
+    id: string;
+    author: TranscriptAuthor;
+    content?: string;
+    embeds?: TranscriptEmbed[];
+    components?: TranscriptComponent[][];
+    editedTimestamp?: number;
+    createdTimestamp: number;
+    flags?: string[];
+    attachments?: TranscriptAttachment[];
+    stickerURLs?: string[];
+    referencedMessage?: string | [string, string, string];  // the message id, the channel id, the guild id
+}
+
+interface TranscriptSchema {
+    code: string;
+    for: TranscriptAuthor;
+    type: "ticket" | "purge"
+    guild: string;
+    channel: string;
+    messages: TranscriptMessage[];
+    createdTimestamp: number;
+    createdBy: TranscriptAuthor;
+}
+
+export class Transcript {
+    transcripts: Collection<TranscriptSchema>;
+
+    constructor() {
+        this.transcripts = database.collection<TranscriptSchema>("transcripts");
+    }
+
+    async create(transcript: Omit<TranscriptSchema, "code">) {
+        // console.log("Transcript create")
+        let code;
+        do {
+            code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
+        } while (await this.transcripts.findOne({ code: code }));
+
+        const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions);
+        if(doc.acknowledged) return code;
+        else return null;
+    }
+
+    async read(code: string) {
+        // console.log("Transcript read")
+        return await this.transcripts.findOne({ code: code });
+    }
+
+    async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {
+        const interactionMember = await interaction.guild?.members.fetch(interaction.user.id)
+        const newOut: Omit<TranscriptSchema, "code"> = {
+            type: "ticket",
+            for: {
+                username: member!.user.username,
+                discriminator: parseInt(member!.user.discriminator),
+                id: member!.user.id,
+                topRole: {
+                    color: member!.roles.highest.color
+                },
+                iconURL: member!.user.displayAvatarURL({ forceStatic: true}),
+                bot: member!.user.bot
+            },
+            guild: interaction.guild!.id,
+            channel: interaction.channel!.id,
+            messages: [],
+            createdTimestamp: Date.now(),
+            createdBy: {
+                username: interaction.user.username,
+                discriminator: parseInt(interaction.user.discriminator),
+                id: interaction.user.id,
+                topRole: {
+                    color: interactionMember?.roles.highest.color ?? 0x000000
+                },
+                iconURL: interaction.user.displayAvatarURL({ forceStatic: true}),
+                bot: interaction.user.bot
+            }
+        }
+        if(member.nickname) newOut.for.nickname = member.nickname;
+        if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!;
+        messages.reverse().forEach((message) => {
+            const msg: TranscriptMessage = {
+                id: message.id,
+                author: {
+                    username: message.author.username,
+                    discriminator: parseInt(message.author.discriminator),
+                    id: message.author.id,
+                    topRole: {
+                        color: message.member!.roles.highest.color
+                    },
+                    iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}),
+                    bot: message.author.bot
+                },
+                createdTimestamp: message.createdTimestamp
+            };
+            if(message.member?.nickname) msg.author.nickname = message.member.nickname;
+            if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!;
+            if (message.content) msg.content = message.content;
+            if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => {
+                const obj: TranscriptEmbed = {};
+                if (embed.title) obj.title = embed.title;
+                if (embed.description) obj.description = embed.description;
+                if (embed.fields.length > 0) obj.fields = embed.fields.map(field => {
+                    return {
+                        name: field.name,
+                        value: field.value,
+                        inline: field.inline ?? false
+                    }
+                });
+                if (embed.color) obj.color = embed.color;
+                if (embed.timestamp) obj.timestamp = embed.timestamp
+                if (embed.footer) obj.footer = {
+                    text: embed.footer.text,
+                };
+                if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL;
+                if (embed.author) obj.author = {
+                    name: embed.author.name
+                };
+                if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL;
+                if (embed.author?.url) obj.author!.url = embed.author.url;
+                return obj;
+            });
+            if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => {
+                const obj: TranscriptComponent = {
+                    type: child.type
+                }
+                if (child.type === ComponentType.Button) {
+                    obj.style = child.style;
+                    obj.label = child.label ?? "";
+                } else if (child.type > 2) {
+                    obj.placeholder = child.placeholder ?? "";
+                }
+                return obj
+            }));
+            if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp;
+            msg.flags = message.flags.toArray();
+
+            if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url);
+            if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""];
+            newOut.messages.push(msg);
+        });
+        return newOut;
+    }
+
+    toHumanReadable(transcript: Omit<TranscriptSchema, "code">): string {
+        let out = "";
+        for (const message of transcript.messages) {
+            if (message.referencedMessage) {
+                if (Array.isArray(message.referencedMessage)) {
+                    out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`;
+                }
+                else out += `> [Reply To] ${message.referencedMessage}\n`;
+            }
+            out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`;
+            out += ` [${new Date(message.createdTimestamp).toISOString()}]`;
+            if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`;
+            out += "\n";
+            if (message.content) out += `[Content]\n${message.content}\n\n`;
+            if (message.embeds) {
+                for (const embed of message.embeds) {
+                    out += `[Embed]\n`;
+                    if (embed.title) out += `| Title: ${embed.title}\n`;
+                    if (embed.description) out += `| Description: ${embed.description}\n`;
+                    if (embed.fields) {
+                        for (const field of embed.fields) {
+                            out += `| Field: ${field.name} - ${field.value}\n`;
+                        }
+                    }
+                    if (embed.footer) {
+                        out += `|Footer: ${embed.footer.text}\n`;
+                    }
+                    out += "\n";
+                }
+            }
+            if (message.components) {
+                for (const component of message.components) {
+                    out += `[Component]\n`;
+                    for (const button of component) {
+                        out += `| Button: ${button.label ?? button.description}\n`;
+                    }
+                    out += "\n";
+                }
+            }
+            if (message.attachments) {
+                for (const attachment of message.attachments) {
+                    out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`;
+                }
+            }
+            out += "\n\n"
+        }
+        return out
+    }
+}
+
 export class History {
     histories: Collection<HistorySchema>;
 
@@ -117,6 +376,7 @@
         after?: string | null,
         amount?: string | null
     ) {
+        // console.log("History create");
         await this.histories.insertOne({
             type: type,
             guild: guild,
@@ -127,10 +387,11 @@
             before: before ?? null,
             after: after ?? null,
             amount: amount ?? null
-        });
+        }, collectionOptions);
     }
 
     async read(guild: string, user: string, year: number) {
+        // console.log("History read");
         const entry = (await this.histories
             .find({
                 guild: guild,
@@ -145,10 +406,41 @@
     }
 
     async delete(guild: string) {
+        // console.log("History delete");
         await this.histories.deleteMany({ guild: guild });
     }
 }
 
+interface ScanCacheSchema {
+    addedAt: Date;
+    hash: string;
+    data: boolean;
+    tags: string[];
+}
+
+export class ScanCache {
+    scanCache: Collection<ScanCacheSchema>;
+
+    constructor() {
+        this.scanCache = database.collection<ScanCacheSchema>("scanCache");
+    }
+
+    async read(hash: string) {
+        // console.log("ScanCache read");
+        return await this.scanCache.findOne({ hash: hash });
+    }
+
+    async write(hash: string, data: boolean, tags?: string[]) {
+        // console.log("ScanCache write");
+        await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions);
+    }
+
+    async cleanup() {
+        // console.log("ScanCache cleanup");
+        await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} });
+    }
+}
+
 export class PerformanceTest {
     performanceData: Collection<PerformanceDataSchema>;
 
@@ -157,10 +449,12 @@
     }
 
     async record(data: PerformanceDataSchema) {
+        // console.log("PerformanceTest record");
         data.timestamp = new Date();
-        await this.performanceData.insertOne(data);
+        await this.performanceData.insertOne(data, collectionOptions);
     }
     async read() {
+        // console.log("PerformanceTest read");
         return await this.performanceData.find({}).toArray();
     }
 }
@@ -184,27 +478,161 @@
     }
 
     async create(guild: string, user: string, note: string | null) {
+        // console.log("ModNotes create");
         await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true });
     }
 
     async read(guild: string, user: string) {
+        // console.log("ModNotes read");
         const entry = await this.modNotes.findOne({ guild: guild, user: user });
         return entry?.note ?? null;
     }
+
+    async delete(guild: string) {
+        // console.log("ModNotes delete");
+        await this.modNotes.deleteMany({ guild: guild });
+    }
 }
 
 export class Premium {
     premium: Collection<PremiumSchema>;
+    cache: Map<string, [boolean, string, number, boolean, Date]>;  // Date indicates the time one hour after it was created
+    cacheTimeout = 1000 * 60 * 60;  // 1 hour
 
     constructor() {
         this.premium = database.collection<PremiumSchema>("premium");
+        this.cache = new Map<string, [boolean, string, number, boolean, Date]>();
     }
 
-    async hasPremium(guild: string) {
+    async updateUser(user: string, level: number) {
+        // console.log("Premium updateUser");
+        if(!(await this.userExists(user))) await this.createUser(user, level);
+        await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true });
+    }
+
+    async userExists(user: string): Promise<boolean> {
+        // console.log("Premium userExists");
+        const entry = await this.premium.findOne({ user: user });
+        return entry ? true : false;
+    }
+
+    async createUser(user: string, level: number) {
+        // console.log("Premium createUser");
+        await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions);
+    }
+
+    async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> {
+        // console.log("Premium hasPremium");
+        // [Has premium, user giving premium, level, is mod: if given automatically]
+        const cached = this.cache.get(guild);
+        if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]];
+        const entries = await this.premium.find({}).toArray();
+        const members = (await client.guilds.fetch(guild)).members.cache
+        for(const {user} of entries) {
+            const member = members.get(user);
+            if(member) { //TODO: Notify user if they've given premium to a server that has since gotten premium via a mod.
+                const modPerms = //TODO: Create list in config for perms
+                            member.permissions.has("Administrator") ||
+                            member.permissions.has("ManageChannels") ||
+                            member.permissions.has("ManageRoles") ||
+                            member.permissions.has("ManageEmojisAndStickers") ||
+                            member.permissions.has("ManageWebhooks") ||
+                            member.permissions.has("ManageGuild") ||
+                            member.permissions.has("KickMembers") ||
+                            member.permissions.has("BanMembers") ||
+                            member.permissions.has("ManageEvents") ||
+                            member.permissions.has("ManageMessages") ||
+                            member.permissions.has("ManageThreads")
+                const entry = entries.find(e => e.user === member.id);
+                if(entry && (entry.level === 3) && modPerms) {
+                    this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]);
+                    return [true, member.id, entry.level, true];
+                }
+            }
+        }
         const entry = await this.premium.findOne({
-            appliesTo: { $in: [guild] }
+            appliesTo: {
+                $elemMatch: {
+                    $eq: guild
+                }
+            }
         });
-        return entry !== null;
+        this.cache.set(guild, [entry ? true : false, entry?.user ?? "", entry?.level ?? 0, false, new Date(Date.now() + this.cacheTimeout)]);
+        return entry ? [true, entry.user, entry.level, false] : null;
+    }
+
+    async fetchUser(user: string): Promise<PremiumSchema | null> {
+        // console.log("Premium fetchUser");
+        const entry = await this.premium.findOne({ user: user });
+        if (!entry) return null;
+        return entry;
+    }
+
+    async checkAllPremium(member?: GuildMember) {
+        // console.log("Premium checkAllPremium");
+        const entries = await this.premium.find({}).toArray();
+        if(member) {
+            const entry = entries.find(e => e.user === member.id);
+            if(entry) {
+                const expiresAt = entry.expiresAt;
+                if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null;
+            }
+            const roles = member.roles;
+            let level = 0;
+            if (roles.cache.has("1066468879309750313")) {
+                level = 99;
+            } else if (roles.cache.has("1066465491713003520")) {
+                level = 1;
+            } else if (roles.cache.has("1066439526496604194")) {
+                level = 2;
+            } else if (roles.cache.has("1066464134322978912")) {
+                level = 3;
+            }
+            await this.updateUser(member.id, level);
+            if (level > 0) {
+                await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}})
+            } else {
+                await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
+            }
+        } else {
+            const members = await (await client.guilds.fetch('684492926528651336')).members.fetch();
+            for(const {roles, id} of members.values()) {
+                const entry = entries.find(e => e.user === id);
+                if(entry) {
+                    const expiresAt = entry.expiresAt;
+                    if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null;
+                }
+                let level: number = 0;
+                if (roles.cache.has("1066468879309750313")) {
+                    level = 99;
+                } else if (roles.cache.has("1066465491713003520")) {
+                    level = 1;
+                } else if (roles.cache.has("1066439526496604194")) {
+                    level = 2;
+                } else if (roles.cache.has("1066464134322978912")) {
+                    level = 3;
+                }
+                await this.updateUser(id, level);
+                if (level > 0) {
+                    await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}})
+                } else {
+                    await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
+                }
+            }
+        }
+    }
+
+    async addPremium(user: string, guild: string) {
+        // console.log("Premium addPremium");
+        const { level } = (await this.fetchUser(user))!;
+        this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]);
+        return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true });
+    }
+
+    removePremium(user: string, guild: string) {
+        // console.log("Premium removePremium");
+        this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]);
+        return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } });
     }
 }
 
@@ -249,7 +677,18 @@
                 channels: string[];
             };
         };
+        clean: {
+            channels: string[];
+            allowed: {
+                users: string[];
+                roles: string[];
+            }
+        }
     };
+    autoPublish: {
+        enabled: boolean;
+        channels: string[];
+    }
     welcome: {
         enabled: boolean;
         role: string | null;
@@ -364,6 +803,6 @@
 export interface PremiumSchema {
     user: string;
     level: number;
-    expires: Date;
     appliesTo: string[];
+    expiresAt?: number;
 }
diff --git a/src/utils/dualCollector.ts b/src/utils/dualCollector.ts
index 714a2d9..0b05779 100644
--- a/src/utils/dualCollector.ts
+++ b/src/utils/dualCollector.ts
@@ -1,4 +1,4 @@
-import Discord, { Client, Interaction, Message, MessageComponentInteraction } from "discord.js";
+import { ButtonInteraction, Client, User, Interaction, InteractionCollector, Message, MessageComponentInteraction, ModalSubmitInteraction } from "discord.js";
 import client from "./client.js";
 
 export default async function (
@@ -10,15 +10,14 @@
     try {
         out = await new Promise((resolve, _reject) => {
             const mes = m
-                .createMessageComponentCollector({
-                    filter: (m) => interactionFilter(m),
-                    time: 300000
-                })
-                .on("collect", (m) => {
+            .createMessageComponentCollector({
+                filter: (m) => interactionFilter(m),
+                time: 300000
+            })
+            .on("collect", (m) => {
                     resolve(m);
                 });
-            const int = m.channel
-                .createMessageCollector({
+            const int = m.channel.createMessageCollector({
                     filter: (m) => messageFilter(m),
                     time: 300000
                 })
@@ -45,35 +44,41 @@
     return out;
 }
 
+function defaultInteractionFilter(i: MessageComponentInteraction, user: User, m: Message) {
+    return i.channel!.id === m.channel!.id && i.user.id === user.id
+}
+function defaultModalFilter(i: ModalSubmitInteraction, user: User, m: Message) {
+    return i.channel!.id === m.channel!.id && i.user.id === user.id
+}
+
+
 export async function modalInteractionCollector(
-    m: Message,
-    modalFilter: (i: Interaction) => boolean | Promise<boolean>,
-    interactionFilter: (i: MessageComponentInteraction) => boolean | Promise<boolean>
-): Promise<null | Interaction> {
-    let out: Interaction;
+    m: Message, user: User,
+    modalFilter?: (i: Interaction) => boolean | Promise<boolean>,
+    interactionFilter?: (i: MessageComponentInteraction) => boolean | Promise<boolean>
+): Promise<null | ButtonInteraction | ModalSubmitInteraction> {
+    let out: ButtonInteraction | ModalSubmitInteraction;
     try {
         out = await new Promise((resolve, _reject) => {
             const int = m
                 .createMessageComponentCollector({
-                    filter: (i: MessageComponentInteraction) => interactionFilter(i),
+                    filter: (i: MessageComponentInteraction) => (interactionFilter ? interactionFilter(i) : true) && defaultInteractionFilter(i, user, m),
                     time: 300000
                 })
-                .on("collect", (i: Interaction) => {
+                .on("collect", async (i: ButtonInteraction) => {
+                    mod.stop();
+                    int.stop();
+                    await i.deferUpdate();
                     resolve(i);
                 });
-            const mod = new Discord.InteractionCollector(client as Client, {
-                filter: (i: Interaction) => modalFilter(i),
+            const mod = new InteractionCollector(client as Client, {
+                filter: (i: Interaction) => (modalFilter ? modalFilter(i) : true) && i.isModalSubmit() && defaultModalFilter(i, user, m),
                 time: 300000
-            }).on("collect", async (i: Interaction) => {
+            }).on("collect", async (i: ModalSubmitInteraction) => {
                 int.stop();
-                (i as Discord.ModalSubmitInteraction).deferUpdate();
-                resolve(i as Discord.ModalSubmitInteraction);
-            });
-            int.on("end", () => {
                 mod.stop();
-            });
-            mod.on("end", () => {
-                int.stop();
+                await i.deferUpdate();
+                resolve(i);
             });
         });
     } catch (e) {
diff --git a/src/utils/ellipsis.ts b/src/utils/ellipsis.ts
new file mode 100644
index 0000000..6ec5888
--- /dev/null
+++ b/src/utils/ellipsis.ts
@@ -0,0 +1,4 @@
+export default (str: string, max: number): string => {
+    if (str.length <= max) return str;
+    return str.slice(0, max - 3) + "...";
+}
\ No newline at end of file
diff --git a/src/utils/eventScheduler.ts b/src/utils/eventScheduler.ts
index 3c9d6ca..a79a260 100644
--- a/src/utils/eventScheduler.ts
+++ b/src/utils/eventScheduler.ts
@@ -2,7 +2,7 @@
 import client from "./client.js";
 import * as fs from "fs";
 import * as path from "path";
-import config from "../config/main.json" assert { type: "json" };
+import config from "../config/main.js";
 
 class EventScheduler {
     private agenda: Agenda;
@@ -10,11 +10,10 @@
     constructor() {
         this.agenda = new Agenda({
             db: {
-                address: config.mongoUrl + "Nucleus",
+                address: config.mongoOptions.host,
                 collection: "eventScheduler"
             }
         });
-
         this.agenda.define("unmuteRole", async (job) => {
             const guild = await client.guilds.fetch(job.attrs.data.guild);
             const user = await guild.members.fetch(job.attrs.data.user);
@@ -43,12 +42,12 @@
                     calculateType: "guildMemberPunish",
                     color: NucleusColors.green,
                     emoji: "PUNISH.MUTE.GREEN",
-                    timestamp: new Date().getTime()
+                    timestamp: Date.now()
                 },
                 list: {
                     memberId: entry(user.user.id, `\`${user.user.id}\``),
                     name: entry(user.user.id, renderUser(user.user)),
-                    unmuted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+                    unmuted: entry(Date.now().toString(), renderDelta(Date.now())),
                     unmutedBy: entry(null, "*Time out ended*")
                 },
                 hidden: {
diff --git a/src/utils/generateEmojiEmbed.ts b/src/utils/generateEmojiEmbed.ts
index c0f17ae..a326fc5 100644
--- a/src/utils/generateEmojiEmbed.ts
+++ b/src/utils/generateEmojiEmbed.ts
@@ -1,4 +1,4 @@
-import { EmbedBuilder } from "@discordjs/builders";
+import { EmbedBuilder } from "discord.js";
 import getEmojiByName from "./getEmojiByName.js";
 
 const colors = {
@@ -13,13 +13,16 @@
     description = "";
 
     _generateTitle() {
+        if (this._emoji && !this._title) return getEmojiByName(this._emoji)
         if (this._emoji) { return `${getEmojiByName(this._emoji)} ${this._title}`; }
-        return this._title;
+        if (this._title) { return this._title };
+        return "";
     }
 
     override setTitle(title: string) {
         this._title = title;
-        super.setTitle(this._generateTitle());
+        const proposedTitle = this._generateTitle();
+        if (proposedTitle) super.setTitle(proposedTitle);
         return this;
     }
     override setDescription(description: string) {
@@ -29,7 +32,8 @@
     }
     setEmoji(emoji: string) {
         this._emoji = emoji;
-        super.setTitle(this._generateTitle());
+        const proposedTitle = this._generateTitle();
+        if (proposedTitle) super.setTitle(proposedTitle);
         return this;
     }
     setStatus(color: "Danger" | "Warning" | "Success") {
diff --git a/src/utils/getCommandDataByName.ts b/src/utils/getCommandDataByName.ts
new file mode 100644
index 0000000..da3e54b
--- /dev/null
+++ b/src/utils/getCommandDataByName.ts
@@ -0,0 +1,28 @@
+import type Discord from "discord.js";
+import client from "./client.js";
+
+
+export const getCommandMentionByName = (name: string): string => {
+    const split = name.replaceAll("/", " ").split(" ")
+    const commandName: string = split[0]!;
+
+    const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName;
+
+    const command = client.fetchedCommands.filter(c => filterCommand(c))
+    if (command.size === 0) return `\`/${name.replaceAll("/", " ")}\``;
+    const commandID = command.first()!.id;
+    return `</${split.join(" ")}:${commandID}>`;
+}
+
+export const getCommandByName = (name: string): {name: string, description: string, mention: string} => {
+
+    const split = name.replaceAll(" ", "/")
+    const command = client.commands["commands/" + split]!;
+    // console.log(command)
+    const mention = getCommandMentionByName(name);
+    return {
+        name: command[1].name,
+        description: command[1].description,
+        mention: mention
+    }
+}
\ No newline at end of file
diff --git a/src/utils/getCommandMentionByName.ts b/src/utils/getCommandMentionByName.ts
deleted file mode 100644
index b2b9937..0000000
--- a/src/utils/getCommandMentionByName.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type Discord from "discord.js";
-import client from "./client.js";
-import config from "../config/main.json" assert { type: "json"};
-
-
-export const getCommandMentionByName = async (name: string): Promise<string> => {
-    const split = name.replaceAll("/", " ").split(" ")
-    const commandName: string = split[0]!;
-    let commandID: string;
-
-    const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName;
-
-    if (config.enableDevelopment) {
-        const developmentGuild = client.guilds.cache.get(config.developmentGuildID)!;
-        await developmentGuild.commands.fetch();
-        commandID = developmentGuild.commands.cache.filter(c => filterCommand(c)).first()!.id;
-    } else {
-        await client.application?.commands.fetch();
-        commandID = client.application?.commands.cache.filter(c => filterCommand(c)).first()!.id!;
-    }
-    return `</${split.join(" ")}:${commandID}>`;
-}
\ No newline at end of file
diff --git a/src/utils/getEmojiByName.ts b/src/utils/getEmojiByName.ts
index 3fa2b53..9df17a4 100644
--- a/src/utils/getEmojiByName.ts
+++ b/src/utils/getEmojiByName.ts
@@ -1,5 +1,7 @@
 import emojis from "../config/emojis.json" assert { type: "json" };
+import lodash from 'lodash';
 
+const isArray = lodash.isArray;
 interface EmojisIndex {
     [key: string]: string | EmojisIndex | EmojisIndex[];
 }
@@ -12,7 +14,7 @@
         if (typeof id === "string" || id === undefined) {
             throw new Error(`Emoji ${name} not found`);
         }
-        if (Array.isArray(id)) {
+        if (isArray(id)) {
             id = id[parseInt(part)];
         } else {
             id = id[part];
@@ -21,6 +23,10 @@
     if (typeof id !== "string" && id !== undefined) {
         throw new Error(`Emoji ${name} not found`);
     }
+    return getEmojiFromId(id, format);
+}
+
+function getEmojiFromId(id: string | undefined, format?: string): string {
     if (format === "id") {
         if (id === undefined) return "0";
         return id.toString();
diff --git a/src/utils/listToAndMore.ts b/src/utils/listToAndMore.ts
new file mode 100644
index 0000000..791ce40
--- /dev/null
+++ b/src/utils/listToAndMore.ts
@@ -0,0 +1,7 @@
+export default (list: string[], max: number) => {
+    // PineappleFan, Coded, Mini (and 10 more)
+    if(list.length > max) {
+        return list.slice(0, max).join(", ") + ` (and ${list.length - max} more)`;
+    }
+    return list.join(", ");
+}
\ No newline at end of file
diff --git a/src/utils/log.ts b/src/utils/log.ts
index 54f656a..c6416a1 100644
--- a/src/utils/log.ts
+++ b/src/utils/log.ts
@@ -7,10 +7,37 @@
 
 const wait = promisify(setTimeout);
 
+export interface LoggerOptions {
+    meta: {
+        type: string;
+        displayName: string;
+        calculateType: string;
+        color: number;
+        emoji: string;
+        timestamp: number;
+    };
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    list: any;
+    hidden: {
+        guild: string;
+    },
+    separate?: {
+        start?: string;
+        end?: string;
+    }
+}
+
+async function isLogging(guild: string, type: string): Promise<boolean> {
+    const config = await client.database.guilds.read(guild);
+    if (!config.logging.logs.enabled) return false;
+    if (!config.logging.logs.channel) return false;
+    if (!toHexArray(config.logging.logs.toLog).includes(type)) { return false; }
+    return true;
+}
 
 export const Logger = {
     renderUser(user: Discord.User | string) {
-        if (typeof user === "string") return `${user} [<@${user}>]`;
+        if (typeof user === "string") user = client.users.cache.get(user)!;
         return `${user.username} [<@${user.id}>]`;
     },
     renderTime(t: number) {
@@ -29,10 +56,12 @@
         if (typeof value === "number") value = value.toString();
         return { value: value, displayValue: displayValue };
     },
-    renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel) {
+    renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel | string) {
+        if (typeof channel === "string") channel = client.channels.cache.get(channel) as Discord.GuildChannel | Discord.ThreadChannel;
         return `${channel.name} [<#${channel.id}>]`;
     },
-    renderRole(role: Discord.Role) {
+    renderRole(role: Discord.Role | string, guild?: Discord.Guild | string) {
+        if (typeof role === "string") role = (typeof guild === "string" ? client.guilds.cache.get(guild) : guild)!.roles.cache.get(role)!;
         return `${role.name} [<@&${role.id}>]`;
     },
     renderEmoji(emoji: Discord.GuildEmoji) {
@@ -43,19 +72,14 @@
         yellow: 0xf2d478,
         green: 0x68d49e
     },
-    async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable): Promise<Discord.GuildAuditLogsEntry[]> {
-        await wait(250);
+    async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable, delay?: number): Promise<Discord.GuildAuditLogsEntry[]> {
+        await wait(delay ?? 250);
         const auditLog = (await guild.fetchAuditLogs({ type: event })).entries.map(m => m)
         return auditLog as Discord.GuildAuditLogsEntry[];
     },
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    async log(log: any): Promise<void> {
+    async log(log: LoggerOptions): Promise<void> {
+        if (!await isLogging(log.hidden.guild, log.meta.calculateType)) return;
         const config = await client.database.guilds.read(log.hidden.guild);
-        if (!config.logging.logs.enabled) return;
-        if (!toHexArray(config.logging.logs.toLog).includes(log.meta.calculateType)) {
-            console.log("Not logging this type of event");
-            return;
-        }
         if (config.logging.logs.channel) {
             const channel = (await client.channels.fetch(config.logging.logs.channel)) as Discord.TextChannel | null;
             const description: Record<string, string> = {};
@@ -70,7 +94,7 @@
                 }
             });
             if (channel) {
-                log.separate = log.separate || {};
+                log.separate = log.separate ?? {};
                 const embed = new Discord.EmbedBuilder()
                     .setTitle(`${getEmojiByName(log.meta.emoji)} ${log.meta.displayName}`)
                     .setDescription(
@@ -83,8 +107,8 @@
                 channel.send({ embeds: [embed] });
             }
         }
-    }
+    },
+    isLogging
 };
 
-
 export default {};
diff --git a/src/utils/logTranscripts.ts b/src/utils/logTranscripts.ts
deleted file mode 100644
index 0950664..0000000
--- a/src/utils/logTranscripts.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type Discord from 'discord.js';
-
-export interface JSONTranscriptSchema {
-    messages: {
-        content: string | null;
-        attachments: {
-            url: string;
-            name: string;
-            size: number;
-        }[];
-        authorID: string;
-        authorUsername: string;
-        authorUsernameColor: string;
-        timestamp: string;
-        id: string;
-        edited: boolean;
-    }[];
-    channel: string;
-    guild: string;
-    timestamp: string;
-}
-
-
-export const JSONTranscriptFromMessageArray = (messages: Discord.Message[]): JSONTranscriptSchema | null => {
-    if (messages.length === 0) return null;
-    return {
-        guild: messages[0]!.guild!.id,
-        channel: messages[0]!.channel.id,
-        timestamp: Date.now().toString(),
-        messages: messages.map((message: Discord.Message) => {
-            return {
-                content: message.content,
-                attachments: message.attachments.map((attachment: Discord.Attachment) => {
-                    return {
-                        url: attachment.url,
-                        name: attachment.name!,
-                        size: attachment.size,
-                    };
-                }),
-                authorID: message.author.id,
-                authorUsername: message.author.username + "#" + message.author.discriminator,
-                authorUsernameColor: message.member!.displayHexColor.toString(),
-                timestamp: message.createdTimestamp.toString(),
-                id: message.id,
-                edited: message.editedTimestamp ? true : false,
-            };
-        })
-    };
-}
-
-export const JSONTranscriptToHumanReadable = (data: JSONTranscriptSchema): string => {
-    let out = "";
-
-    for (const message of data.messages) {
-        const date = new Date(parseInt(message.timestamp));
-        out += `${message.authorUsername} (${message.authorID}) [${date}]`;
-        if (message.edited) out += " (edited)";
-        if (message.content) out += "\nContent:\n" + message.content.split("\n").map((line: string) => `\n> ${line}`).join("");
-        if (message.attachments.length > 0) out += "\nAttachments:\n" + message.attachments.map((attachment: { url: string; name: string; size: number; }) => `\n> [${attachment.name}](${attachment.url}) (${attachment.size} bytes)`).join("\n");
-
-        out += "\n\n";
-    }
-    return out;
-}
\ No newline at end of file
diff --git a/src/utils/memory.ts b/src/utils/memory.ts
index 870ffaf..60a6535 100644
--- a/src/utils/memory.ts
+++ b/src/utils/memory.ts
@@ -7,6 +7,7 @@
     logging: GuildConfig["logging"];
     tickets: GuildConfig["tickets"];
     tags: GuildConfig["tags"];
+    autoPublish: GuildConfig["autoPublish"];
 }
 
 class Memory {
@@ -31,7 +32,8 @@
                 filters: guildData.filters,
                 logging: guildData.logging,
                 tickets: guildData.tickets,
-                tags: guildData.tags
+                tags: guildData.tags,
+                autoPublish: guildData.autoPublish
             });
         }
         return this.memory.get(guild)!;
diff --git a/src/utils/performanceTesting/record.ts b/src/utils/performanceTesting/record.ts
index 95761e9..71883c5 100644
--- a/src/utils/performanceTesting/record.ts
+++ b/src/utils/performanceTesting/record.ts
@@ -2,7 +2,7 @@
 import * as CP from 'child_process';
 import * as process from 'process';
 import systeminformation from "systeminformation";
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
 import singleNotify from "../singleNotify.js";
 
 
@@ -39,7 +39,7 @@
         singleNotify(
             "performanceTest",
             config.developmentGuildID,
-            `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${results.resources.memory}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``,
+            `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${Math.round(results.resources.memory)}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``,
             "Critical",
             config.owners
         )
diff --git a/src/utils/singleNotify.ts b/src/utils/singleNotify.ts
index 8e3aa60..6bf63e1 100644
--- a/src/utils/singleNotify.ts
+++ b/src/utils/singleNotify.ts
@@ -1,6 +1,7 @@
 import client from "./client.js";
 import EmojiEmbed from "./generateEmojiEmbed.js";
 import { Record as ImmutableRecord } from "immutable";
+import type { TextChannel, ThreadChannel, NewsChannel } from "discord.js";
 
 const severitiesType = ImmutableRecord({
     Critical: "Danger",
@@ -31,20 +32,20 @@
         const channel = await client.channels.fetch(data.logging.staff.channel);
         if (!channel) return;
         if (!channel.isTextBased()) return;
+        const textChannel = channel as TextChannel | ThreadChannel | NewsChannel;
+        let messageData = {embeds: [
+            new EmojiEmbed()
+                .setTitle(`${severity} notification`)
+                .setDescription(message)
+                .setStatus(severities.get(severity))
+                .setEmoji("CONTROL.BLOCKCROSS")
+        ]}
         if (pings) {
-            await channel.send({
+            messageData = Object.assign(messageData, {
                 content: pings.map((ping) => `<@${ping}>`).join(" ")
             });
         }
-        await channel.send({
-            embeds: [
-                new EmojiEmbed()
-                    .setTitle(`${severity} notification`)
-                    .setDescription(message)
-                    .setStatus(severities.get(severity))
-                    .setEmoji("CONTROL.BLOCKCROSS")
-            ]
-        });
+        await textChannel.send(messageData);
     } catch (err) {
         console.error(err);
     }
diff --git a/src/utils/temp/generateFileName.ts b/src/utils/temp/generateFileName.ts
index 3aab64c..109478d 100644
--- a/src/utils/temp/generateFileName.ts
+++ b/src/utils/temp/generateFileName.ts
@@ -12,7 +12,7 @@
     if (fs.existsSync(`./${fileName}`)) {
         fileName = generateFileName(ending);
     }
-    client.database.eventScheduler.schedule("deleteFile", (new Date().getTime() + 60 * 1000).toString(), {
+    client.database.eventScheduler.schedule("deleteFile", (Date.now() + 60 * 1000).toString(), {
         fileName: `${fileName}.${ending}`
     });
     return path.join(__dirname, fileName + "." + ending);