Development (#62)

- fixed /user tracks & /settings tracks. Added presets to /server
buttons
- prettiered
diff --git a/src/commands/server/buttons.ts b/src/commands/server/buttons.ts
index 74c255c..f07f3ce 100644
--- a/src/commands/server/buttons.ts
+++ b/src/commands/server/buttons.ts
@@ -20,6 +20,7 @@
 import lodash from "lodash";
 import getEmojiByName from "../../utils/getEmojiByName.js";
 import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import _ from "lodash";
 
 export const command = new SlashCommandSubcommandBuilder()
     .setName("buttons")
@@ -50,6 +51,27 @@
     createticket: "Create Ticket"
 };
 
+const presetButtons = [
+    {
+        title: "Verify",
+        description: "Click the button below to get verified in the server.",
+        buttons: ["verifybutton"],
+        color: "GREEN"
+    },
+    {
+        title: "Get Roles",
+        description: "Click the button to choose which roles you would like in the server",
+        buttons: ["rolemenu"],
+        color: "BLUE"
+    },
+    {
+        title: "Create Ticket",
+        description: "Click the button below to create a ticket",
+        buttons: ["createticket"],
+        color: "RED"
+    }
+];
+
 export const callback = async (interaction: CommandInteraction): Promise<void> => {
     const m = await interaction.reply({
         embeds: LoadingEmbed,
@@ -58,7 +80,7 @@
     });
 
     let closed = false;
-    const data: Data = {
+    let data: Data = {
         buttons: [],
         title: null,
         description: null,
@@ -71,7 +93,7 @@
                 .setCustomId("edit")
                 .setLabel("Edit Embed")
                 .setStyle(ButtonStyle.Secondary)
-                .setEmoji(getEmojiByName("ICONS.EDIT") as APIMessageComponentEmoji),
+                .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
             new ButtonBuilder()
                 .setCustomId("send")
                 .setLabel("Send")
@@ -95,6 +117,22 @@
                 )
         );
 
+        const presetSelect = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
+            new StringSelectMenuBuilder()
+                .setCustomId("preset")
+                .setPlaceholder("Select a preset")
+                .setMaxValues(1)
+                .addOptions(
+                    presetButtons.map((preset, i) => {
+                        return new StringSelectMenuOptionBuilder()
+                            .setLabel(preset.title)
+                            .setValue(i.toString())
+                            .setDescription(preset.description)
+                            .setEmoji(getEmojiByName("COLORS." + preset.color, "id") as APIMessageComponentEmoji);
+                    })
+                )
+        );
+
         const buttonSelect = new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
             new StringSelectMenuBuilder()
                 .setCustomId("button")
@@ -141,7 +179,7 @@
 
         await interaction.editReply({
             embeds: [embed],
-            components: [colorSelect, buttonSelect, channelMenu, buttons]
+            components: [presetSelect, colorSelect, buttonSelect, channelMenu, buttons]
         });
 
         let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction;
@@ -257,6 +295,12 @@
                 console.log(err);
             }
             switch (i.customId) {
+                case "preset": {
+                    const chosen = presetButtons[parseInt(i.values[0]!)]!;
+                    const newColor = colors[chosen.color!]!;
+                    data = _.assign(data, chosen, { color: newColor });
+                    break;
+                }
                 case "color": {
                     data.color = colors[i.values[0]!]!;
                     break;
diff --git a/src/commands/settings/tracks.ts b/src/commands/settings/tracks.ts
index 60a7eae..b79f54d 100644
--- a/src/commands/settings/tracks.ts
+++ b/src/commands/settings/tracks.ts
@@ -171,6 +171,7 @@
     const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator");
     if (!current) {
         current = _.cloneDeep(defaultTrackData);
+        current.name = "Default";
     }
 
     const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
@@ -348,16 +349,15 @@
 const callback = async (interaction: CommandInteraction) => {
     const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true });
     const config = await client.database.guilds.read(interaction.guild!.id);
-    const tracks: ObjectSchema[] = config.tracks;
+    const tracks: ObjectSchema[] = _.cloneDeep(config.tracks);
     const roles = await interaction.guild!.roles.fetch();
 
     let page = 0;
     let closed = false;
-    let modified = false;
 
     do {
         const embed = new EmojiEmbed().setTitle("Track Settings").setEmoji("TRACKS.ICON").setStatus("Success");
-        const noTracks = config.tracks.length === 0;
+        const noTracks = tracks.length === 0;
         let current: ObjectSchema;
 
         const pageSelect = new StringSelectMenuBuilder().setCustomId("page").setPlaceholder("Select a track to manage");
@@ -398,7 +398,7 @@
                 .setLabel("Save")
                 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
                 .setStyle(ButtonStyle.Success)
-                .setDisabled(!modified)
+                .setDisabled(_.isEqual(tracks, config.tracks))
         );
         if (noTracks) {
             embed.setDescription(
@@ -471,13 +471,15 @@
                 case "add": {
                     const newPage = await editTrack(i, m, roles);
                     if (_.isEqual(newPage, defaultTrackData)) break;
-                    tracks.push();
+                    if (!newPage) break;
+                    console.log(newPage);
+                    tracks.push(newPage);
+                    console.log(tracks);
                     page = tracks.length - 1;
                     break;
                 }
                 case "save": {
                     await client.database.guilds.write(interaction.guild!.id, { tracks: tracks });
-                    modified = false;
                     await client.memory.forceUpdate(interaction.guild!.id);
                     break;
                 }
@@ -490,7 +492,6 @@
                             const edited = await editTrack(i, m, roles, current!);
                             if (!edited) break;
                             tracks[page] = edited;
-                            modified = true;
                             break;
                         }
                         case "delete": {
diff --git a/src/commands/user/track.ts b/src/commands/user/track.ts
index ee69868..1ce05ee 100644
--- a/src/commands/user/track.ts
+++ b/src/commands/user/track.ts
@@ -97,7 +97,12 @@
                 selected.length
             } roles from this track. `;
             conflictDropdown = [];
-            if (roles.get(selected[0]!)!.position < memberRoles.highest.position || managed) {
+            const yourRoles = guild.members.cache.get(interaction.user.id)!.roles;
+            if (
+                (roles.get(selected[0]!)!.position < yourRoles.highest.position &&
+                    roles.get(selected[0]!)!.position < guild.members.me!.roles.highest.position!) ||
+                managed
+            ) {
                 generated +=
                     "In order to promote or demote this user, you must select which role the member should keep.";
                 selected.forEach((role) => {
@@ -115,8 +120,13 @@
                         .setPlaceholder("Select a role to keep")
                 ];
             } else {
-                generated +=
-                    "You don't have permission to manage one or more of the users roles, and therefore can't select one to keep.";
+                if (roles.get(selected[0]!)!.position >= yourRoles.highest.position) {
+                    generated +=
+                        "You don't have permission to manage one or more of the user's roles, and therefore can't select one to keep.";
+                } else {
+                    generated +=
+                        "I don't have permission to manage one or more of the user's roles, and therefore can't select one to keep.";
+                }
             }
         } else {
             currentRoleIndex = selected.length === 0 ? -1 : data.track.indexOf(selected[0]!.toString());
@@ -183,6 +193,7 @@
             const rolesToRemove = selected.filter(
                 (role) => role !== (component as StringSelectMenuInteraction).values[0]
             );
+
             await member.roles.remove(rolesToRemove);
         } else if (component.customId === "promote") {
             if (
diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts
index 80ca150..1b9d740 100644
--- a/src/reflex/scanners.ts
+++ b/src/reflex/scanners.ts
@@ -210,18 +210,14 @@
 
 export async function doMemberChecks(member: Discord.GuildMember): Promise<void> {
     if (member.user.bot) return;
-    console.log("Checking member " + member.user.tag);
     const guild = member.guild;
     const guildData = await client.database.guilds.read(guild.id);
     if (!guildData.logging.staff.channel) return;
     const [loose, strict] = [guildData.filters.wordFilter.words.loose, guildData.filters.wordFilter.words.strict];
-    console.log(1, loose, strict);
     // Does the username contain filtered words
     const usernameCheck = TestString(member.user.username, loose, strict, guildData.filters.wordFilter.enabled);
-    console.log(2, usernameCheck);
     // Does the nickname contain filtered words
     const nicknameCheck = TestString(member.nickname ?? "", loose, strict, guildData.filters.wordFilter.enabled);
-    console.log(3, nicknameCheck);
     // Does the profile picture contain filtered words
     const avatarTextCheck = TestString(
         (await TestImage(member.displayAvatarURL({ forceStatic: true }))) ?? "",
@@ -229,18 +225,14 @@
         strict,
         guildData.filters.wordFilter.enabled
     );
-    console.log(4, avatarTextCheck);
     // Is the profile picture NSFW
     const avatar = member.displayAvatarURL({ extension: "png", size: 1024, forceStatic: true });
     const avatarCheck = guildData.filters.images.NSFW && (await NSFWCheck(avatar));
-    console.log(5, avatarCheck);
     // Does the username contain an invite
     const inviteCheck = guildData.filters.invite.enabled && /discord\.gg\/[a-zA-Z0-9]+/gi.test(member.user.username);
-    console.log(6, inviteCheck);
     // Does the nickname contain an invite
     const nicknameInviteCheck =
         guildData.filters.invite.enabled && /discord\.gg\/[a-zA-Z0-9]+/gi.test(member.nickname ?? "");
-    console.log(7, nicknameInviteCheck);
     if (
         usernameCheck !== null ||
         nicknameCheck !== null ||
@@ -282,39 +274,42 @@
                 `**Member:** ${member.user.username} (<@${member.user.id}>)\n\n` +
                     infractions.map((element) => `${filter} ${element}`).join("\n")
             );
+        const buttons = [
+            new ButtonBuilder()
+                .setCustomId(`mod:warn:${member.user.id}`)
+                .setLabel("Warn")
+                .setStyle(ButtonStyle.Primary),
+            new ButtonBuilder()
+                .setCustomId(`mod:mute:${member.user.id}`)
+                .setLabel("Mute")
+                .setStyle(ButtonStyle.Primary),
+            new ButtonBuilder().setCustomId(`mod:kick:${member.user.id}`).setLabel("Kick").setStyle(ButtonStyle.Danger),
+            new ButtonBuilder().setCustomId(`mod:ban:${member.user.id}`).setLabel("Ban").setStyle(ButtonStyle.Danger)
+        ];
+        if (usernameCheck !== null || nicknameCheck !== null)
+            buttons.concat([
+                new ButtonBuilder()
+                    .setCustomId(`mod:nickname:${member.user.id}`)
+                    .setLabel("Change Name")
+                    .setStyle(ButtonStyle.Primary)
+            ]);
+        if (avatarCheck || avatarTextCheck !== null)
+            buttons.concat([
+                new ButtonBuilder().setURL(member.displayAvatarURL()).setLabel("View Avatar").setStyle(ButtonStyle.Link)
+            ]);
+        const components: ActionRowBuilder<ButtonBuilder>[] = [];
+
+        for (let i = 0; i < buttons.length; i += 5) {
+            components.push(
+                new ActionRowBuilder<ButtonBuilder>().addComponents(
+                    buttons.slice(i, Math.min(buttons.length - 1, i + 5))
+                )
+            );
+        }
+
         await channel.send({
             embeds: [embed],
-            components: [
-                new ActionRowBuilder<ButtonBuilder>().addComponents(
-                    ...[
-                        new ButtonBuilder()
-                            .setCustomId(`mod:warn:${member.user.id}`)
-                            .setLabel("Warn")
-                            .setStyle(ButtonStyle.Primary),
-                        new ButtonBuilder()
-                            .setCustomId(`mod:mute:${member.user.id}`)
-                            .setLabel("Mute")
-                            .setStyle(ButtonStyle.Primary),
-                        new ButtonBuilder()
-                            .setCustomId(`mod:kick:${member.user.id}`)
-                            .setLabel("Kick")
-                            .setStyle(ButtonStyle.Danger),
-                        new ButtonBuilder()
-                            .setCustomId(`mod:ban:${member.user.id}`)
-                            .setLabel("Ban")
-                            .setStyle(ButtonStyle.Danger)
-                    ].concat(
-                        usernameCheck !== null || nicknameCheck !== null
-                            ? [
-                                  new ButtonBuilder()
-                                      .setCustomId(`mod:nickname:${member.user.id}`)
-                                      .setLabel("Change Name")
-                                      .setStyle(ButtonStyle.Primary)
-                              ]
-                            : []
-                    )
-                )
-            ]
+            components: components
         });
     }
 }
diff --git a/src/utils/getEmojiByName.ts b/src/utils/getEmojiByName.ts
index 99d1215..9bbf61f 100644
--- a/src/utils/getEmojiByName.ts
+++ b/src/utils/getEmojiByName.ts
@@ -1,7 +1,6 @@
 import emojis from "../config/emojis.json" assert { type: "json" };
-import lodash from "lodash";
+import _ from "lodash";
 
-const isArray = lodash.isArray;
 interface EmojisIndex {
     [key: string]: string | EmojisIndex | EmojisIndex[];
 }
@@ -14,7 +13,7 @@
         if (typeof id === "string" || id === undefined) {
             throw new Error(`Emoji ${name} not found`);
         }
-        if (isArray(id)) {
+        if (_.isArray(id)) {
             id = id[parseInt(part)];
         } else {
             id = id[part];
@@ -34,9 +33,9 @@
     if (id === undefined) {
         return "";
     } else if (id.toString().startsWith("a")) {
-        return `<a:_:${id.toString().slice(1, id.toString().length)}>`;
+        return `<a:N:${id.toString().slice(1, id.toString().length)}>`;
     }
-    return `<:_:${id}>`;
+    return `<:N:${id}>`;
 }
 
 export default getEmojiByName;