added messageEdit highlighting (#99)

diff --git a/package.json b/package.json
index 3591ee0..968af14 100644
--- a/package.json
+++ b/package.json
@@ -5,13 +5,12 @@
         "@tensorflow/tfjs": "^3.18.0",
         "@tensorflow/tfjs-node": "^3.18.0",
         "@total-typescript/ts-reset": "^0.3.7",
-        "@types/gm": "^1.25.0",
-        "@types/node": "^18.14.6",
         "@ungap/structured-clone": "^1.0.1",
         "agenda": "^4.3.0",
         "body-parser": "^1.20.0",
         "canvas": "^2.11.0",
         "clamscan": "^2.1.2",
+        "diff": "^5.1.0",
         "discord.js": "^14.8.0",
         "eslint": "^8.21.0",
         "express": "^4.18.1",
@@ -83,6 +82,9 @@
         "prettier-eslint": "^15.0.1",
         "tsc-suppress": "^1.0.7",
         "typescript": "^5.0.0",
-        "yarn-audit-fix": "^9.3.9"
+        "yarn-audit-fix": "^9.3.9",
+        "@types/diff": "^5.0.3",
+        "@types/gm": "^1.25.0",
+        "@types/node": "^18.14.6"
     }
 }
diff --git a/src/events/messageEdit.ts b/src/events/messageEdit.ts
index 270da00..bc85353 100644
--- a/src/events/messageEdit.ts
+++ b/src/events/messageEdit.ts
@@ -1,6 +1,7 @@
 import type { NucleusClient } from "../utils/client.js";
 import type { Message, MessageReference } from "discord.js";
 import type Discord from "discord.js";
+import * as diff from "diff";
 
 export const event = "messageUpdate";
 
@@ -10,10 +11,9 @@
     if (!newMessage.guild) return;
     const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderNumberDelta, renderChannel } =
         client.logger;
-
     const replyTo: MessageReference | null = newMessage.reference;
-    let newContent = newMessage.cleanContent.replaceAll("`", "‘");
-    let oldContent = oldMessage.cleanContent.replaceAll("`", "‘");
+    const newContent = newMessage.cleanContent.replaceAll("`", "‘");
+    const oldContent = oldMessage.cleanContent.replaceAll("`", "‘");
     let attachmentJump = "";
     const config = (await client.database.guilds.read(newMessage.guild.id)).logging.attachments.saved[
         newMessage.channel.id + newMessage.id
@@ -62,46 +62,69 @@
     if (!newMessage.editedTimestamp) {
         return;
     }
-    if (newContent.length > 256) newContent = newContent.substring(0, 253) + "...";
-    if (oldContent.length > 256) oldContent = oldContent.substring(0, 253) + "...";
-    const data = {
-        meta: {
-            type: "messageUpdate",
-            displayName: "Message Edited",
-            calculateType: "messageUpdate",
-            color: NucleusColors.yellow,
-            emoji: "MESSAGE.EDIT",
-            timestamp: newMessage.editedTimestamp
-        },
-        separate: {
-            start:
-                (oldContent
-                    ? `**Before:**\n\`\`\`\n${oldContent}\n\`\`\`\n`
-                    : "**Before:** *Message had no content*\n") +
-                (newContent ? `**After:**\n\`\`\`\n${newContent}\n\`\`\`` : "**After:** *Message had no content*"),
-            end: `[[Jump to message]](${newMessage.url})`
-        },
-        list: {
-            messageId: entry(newMessage.id, `\`${newMessage.id}\``),
-            sentBy: entry(newMessage.author.id, renderUser(newMessage.author)),
-            sentIn: entry(newMessage.channel.id, renderChannel(newMessage.channel as Discord.GuildBasedChannel)),
-            sent: entry(newMessage.createdTimestamp, renderDelta(newMessage.createdTimestamp)),
-            edited: entry(newMessage.editedTimestamp, renderDelta(newMessage.editedTimestamp)),
-            mentions: renderNumberDelta(oldMessage.mentions.users.size, newMessage.mentions.users.size),
-            attachments: entry(
-                renderNumberDelta(oldMessage.attachments.size, newMessage.attachments.size),
-                renderNumberDelta(oldMessage.attachments.size, newMessage.attachments.size) + attachmentJump
-            ),
-            repliedTo: entry(
-                replyTo ? replyTo.messageId! : null,
-                replyTo
-                    ? `[[Jump to message]](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${replyTo.messageId})`
-                    : "None"
-            )
-        },
-        hidden: {
-            guild: newMessage.guild.id
-        }
-    };
-    await log(data);
+    const differences = diff.diffChars(oldContent, newContent);
+    console.log(differences);
+    let contentEdit = "";
+    if (differences.map((d) => (d.added || d.removed ? 1 : 0)).filter((f) => f === 1).length > 0) {
+        const green = "\x1B[36m";
+        const red = "\x1B[41m";
+        const skipped = "\x1B[40;33m";
+        const reset = "\x1B[0m";
+        const bold = "\x1B[1m";
+        const cutoff = 20;
+        differences.forEach((part) => {
+            if (!part.added && !part.removed && part.value.length > cutoff) {
+                contentEdit +=
+                    reset +
+                    part.value.slice(0, cutoff / 2) +
+                    skipped +
+                    `(${part.value.length - cutoff} more)` +
+                    reset +
+                    part.value.slice(-(cutoff / 2));
+            } else {
+                if (part.added || part.removed) {
+                    part.value = part.value.replaceAll(" ", "▁");
+                }
+                contentEdit += (part.added ? green : part.removed ? red : reset) + part.value;
+            }
+        });
+        contentEdit = contentEdit.slice(0, 2000);
+        contentEdit += `\n\n${bold}Key:${reset} ${green}Added${reset} | ${red}Removed${reset} | ${skipped}Skipped${reset}`;
+        const data = {
+            meta: {
+                type: "messageUpdate",
+                displayName: "Message Edited",
+                calculateType: "messageUpdate",
+                color: NucleusColors.yellow,
+                emoji: "MESSAGE.EDIT",
+                timestamp: newMessage.editedTimestamp
+            },
+            separate: {
+                start: `\`\`\`ansi\n${contentEdit}\`\`\``,
+                end: `[[Jump to message]](${newMessage.url})`
+            },
+            list: {
+                messageId: entry(newMessage.id, `\`${newMessage.id}\``),
+                sentBy: entry(newMessage.author.id, renderUser(newMessage.author)),
+                sentIn: entry(newMessage.channel.id, renderChannel(newMessage.channel as Discord.GuildBasedChannel)),
+                sent: entry(newMessage.createdTimestamp, renderDelta(newMessage.createdTimestamp)),
+                edited: entry(newMessage.editedTimestamp, renderDelta(newMessage.editedTimestamp)),
+                mentions: renderNumberDelta(oldMessage.mentions.users.size, newMessage.mentions.users.size),
+                attachments: entry(
+                    renderNumberDelta(oldMessage.attachments.size, newMessage.attachments.size),
+                    renderNumberDelta(oldMessage.attachments.size, newMessage.attachments.size) + attachmentJump
+                ),
+                repliedTo: entry(
+                    replyTo ? replyTo.messageId! : null,
+                    replyTo
+                        ? `[[Jump to message]](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${replyTo.messageId})`
+                        : "None"
+                )
+            },
+            hidden: {
+                guild: newMessage.guild.id
+            }
+        };
+        await log(data);
+    }
 }
diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts
index 5889d54..47f96ae 100644
--- a/src/utils/commandRegistration/register.ts
+++ b/src/utils/commandRegistration/register.ts
@@ -80,7 +80,13 @@
         i++;
         try {
             console.log(`${last}─ ${colors.yellow}Loading event ${file.name}${colors.none}`);
-            const event = await import(`../../../${config.eventsFolder}/${file.name}`);
+
+            let event;
+            try {
+                event = await import(`../../../${config.eventsFolder}/${file.name}`);
+            } catch (e) {
+                console.error(colors.red + e + colors.none);
+            }
 
             client.on(event.event, event.callback.bind(null, client));
 
@@ -98,7 +104,7 @@
             );
         }
     }
-    console.log(`Loaded ${files.length - errors} events (${errors} failed)`);
+    console.log(`${errors ? colors.red : ""}Loaded ${files.length - errors} events (${errors} failed)${colors.none}`);
 }
 
 async function registerContextMenus() {
@@ -179,7 +185,11 @@
         }
     }
 
-    console.log(`Loaded ${messageFiles.length + userFiles.length - errors} context menus (${errors} failed)`);
+    console.log(
+        `${errors ? colors.red : ""}Loaded ${
+            messageFiles.length + userFiles.length - errors
+        } context menus (${errors} failed) ${colors.none}`
+    );
     return commands;
 }
 
diff --git a/yarn.lock b/yarn.lock
index cedcc5c..14cfaef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1258,6 +1258,11 @@
     "@types/node" "*"
     axios "^0.24.0"
 
+"@types/diff@^5.0.3":
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.3.tgz#1f89e49ff83b5d200d78964fb896c68498ce1828"
+  integrity sha512-amrLbRqTU9bXMCc6uX0sWpxsQzRIo9z6MJPkH1pkez/qOxuqSZVuryJAWoBRq94CeG8JxY+VK4Le9HtjQR5T9A==
+
 "@types/eslint@^8.4.2":
   version "8.21.1"
   resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz"
@@ -2190,6 +2195,11 @@
   resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz"
   integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
 
+diff@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
+  integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"