# Advanced implementation

First of all, congratulations! You've managed to implement our structure in your application in a 100% functional way. Now, this guide is for those who are already familiar with the structure and want to upgrade their functionality. Unfortunately, this module doesn't feature many images (we believe you've already gained enough familiarity with the structure to understand and fit the methods on your own), but we'll still strive to be as clear as possible to leave no doubts.

# Adding Utilities

The Utils folder in our project serves a clear and valuable purpose. It acts as a central repository for all our auxiliary functions and utilities. These functions are thoughtfully organized and maintained here to enhance development efficiency, code reuse, and simplified maintenance

In our Visual Studio Code, we will create a new folder within the src directory. This folder will be named Utils.

# Custom logging

Alright, with the Utils folder created, why don't we add a distinct logging system? It will be better to differentiate each action that our structure performs with meaningful colors and clear naming.

So, here we go. Inside our Utils folder, we will create a subfolder named Function with a file named getColors.js to customize our logs.

File Map: src > Utils > Functions > getColors.js
Package: npm i colors (opens new window)
Content:

import colors from 'colors';

const log = (message = message.replace(' ', '⠀'), type = types[0]) => {
  
  const types = ['error', 'system', 'commands', 'firebase', 'cache', 'success', 'client', 'mysql', 'notice'];

  const colorFormat = {
    error: ['[ ❌ Error ]'.bgRed, 'red'],
    system: ['[ 💻 System ]'.bgBlue, 'blue'],
    commands: ['[ 🤖 Commands ]'.bgCyan, 'cyan'],
    cache: ['[ 📙 Cache ]'.bgGreen, 'green'],
    success: ['[ ✔️ Success ]'.bgGreen, 'green'],
    client: ['[ 💁 Client ]'.bgMagenta, 'magenta'],
    notice: ['[ 🔔 Notice ]'.bgYellow + '⠀➜ '.italic.red, 'yellow']
  };

  if (!types.includes(type)) {
    type = types[0];
  }

  const [typeString, color] = colorFormat[type];

  console.log(`${typeString}${colors[color](message)}`);
}

export default log;

If you want to implement it in any of your systems already, feel free to do so. In the next module, we will implement it in the classes.

# Creating mosaic

Have you ever seen those Neofetch-style mosaics and been captivated, imagining yourself creating one? Well, now we're going to implement a Discord mosaic. To start, let's stay in the Functions folder and create a file called createMosaic.js.

File Map: src > Utils > Functions > createMosaic.js
Package: npm i moment (opens new window)
Content:

import os from 'os';

import moment from 'moment';
moment.locale('pt-BR');

const createMosaic = `
\x1b[0;95m⠀⠀⠀⠀⢀⣀⣤⣤⡀⠀⠀⠀⠀⢀⣤⣤⣀⡀⠀⠀⠀⠀  A guide for donkeys\x1b[0m
\x1b[0;95m⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀  \x1b[0m-----------------------
\x1b[0;95m⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀  OS\x1b[0m: ${os.platform()}
\x1b[0;95m⢀⣿⣿⣿⣿⣿⡿⠿⣿⣿⣿⣿⣿⣿⠿⢿⣿⣿⣿⣿⣿⡀  Pterodactyl\x1b[0m: ${moment(Date.now()).format('LL')}
\x1b[0;95m⣸⣿⣿⣿⣿⡏⠀⠀⠀⢻⣿⣿⡟⠀⠀⠀⢹⣿⣿⣿⣿⣇  Arch:\x1b[0m: ${os.arch()}
\x1b[0;95m⣿⣿⣿⣿⣿⣧⡀⠀⣀⣾⣿⣿⣷⣀⠀⢀⣼⣿⣿⣿⣿⣿  Author\x1b[0m: only.don
\x1b[0;95m⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿  Terminal\x1b[0m: code block
\x1b[0;95m⠙⠻⢿⣿⣿⣶⡭⠙⠛⠛⠛⠛⠛⠛⠋⢭⣶⣿⣿⡿⠟⠋  \x1b[0m
\x1b[0;95m⠀⠀⠀⠈⠙⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠋⠁⠀⠀⠀    \x1b[0;30m███\x1b[0;31m███\x1b[0;32m███\x1b[0;34m███\x1b[0;35m███\x1b[0;36m███\x1b[0;37m███
`

export default createMosaic;

How wonderful, isn't it? Let's add it to our ready event before registering the slash commands. Like this:

import EventMap from '../../Structure/EventMap.js';
import terminal from '../../Utils/Functions/createMosaic.js';

export default class extends EventMap {
  constructor(client) {
    super(client, {
      name: 'ready',
      once: true
    });
  }
  run = async () => {
    console.log(terminal)  
      
    await this.client.registerCommands();
    console.log(`Ready! Logged in as ${this.client.user.tag}`); 
  };
};

# Implementing a cache system

Okay, we've configured the mosaic, which, by the way, looks fantastic. Now, let's set up the caching system. In this step, we will only create the file where the command cache will be stored.

File Map: src > Utils > .cache > .commandsCache.json
Content:

{}

Well, for now, that's it. In the next module, you will start using this file. Remember that .cache is a folder and not a file.

# Structure advanced

Well, now we will update and enhance our classes Client, EventMap, PrefixCommands and SlashCommands for better performance.

# Separating configuration file

Well, let's split the configuration file to avoid redundant settings and organize our files more efficiently.

File Map: src > Config > Config.json
Content:

{
  "default_prefix": "--",
  "default_developers": ["828677274659586068"]
}

Here, we've introduced two objects: default_prefix for the application's prefix, which we'll use when updating our messageCreate, and default_developers, an array of predefined configurations containing user IDs with developer permissions within our application.

# Client

Alright, now let's enhance our client class. It's quite simple, and we can improve it. First, let's go back to our Client.js file and add our logging system to the constructor like this:

import log from '../Utils/Functions/getColors.js';

export default class extends Client {
    constructor(options) {
        super(options);

        this.SlashCommandArray = [];
        this.PrefixCommandArray = [];
        this.getPrefixCommands();
        this.getSlashCommands();
        this.getEvents();
        this.log = log;
        this.cooldown = new Set();
    }

Alright, now it's time to update our registerCommands and implement a system of local and global scopes, as well as a caching system to avoid unnecessary requests to the Discord API. First, let's implement a new method called cacheCommands to separate commands with local and global scopes to keep the cache more organized, like this:

cacheCommands(commands, isGlobal) {
    let cacheData = JSON.parse(readFileSync(searchFile, 'utf-8'));
    let hasChanges = false;
  
    const cacheObjectName = isGlobal ? 'globalCommandsCache' : 'localCommandsCache';
    const cacheObject = cacheData[cacheObjectName] || {};
  
    for (const name in cacheObject) {
      if (!commands.some(command => command.name === name)) {
        delete cacheObject[name];
        hasChanges = true;
      }
    }
  
    for (const command of commands) {
      const name = command.name;
      const json = JSON.stringify(command, null, 2);
  
      if (!cacheObject.hasOwnProperty(name) || cacheObject[name] !== json) {
        cacheObject[name] = json;
        hasChanges = true;
      }
    }
  
    cacheData[cacheObjectName] = cacheObject;
  
    writeFileSync(searchFile, JSON.stringify(cacheData, null, 2), 'utf8');
  
    return hasChanges;
}

So, let's update the imports like this:

import { readdirSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { Client } from 'discord.js';

import log from '../Utils/Functions/getColors.js';
const searchFile = './src/Utils/.cache/.commandsCache.json';

And finally, let's update our registerCommands to include this implementation of the cache system we created like this:

async registerCommands() {
    this.log('Please wait while the application loads the commands (/)...', 'system');

    const existingCommands = this.application.commands.cache;
    const globalCommands = this.SlashCommandArray.filter(command => !command.guildCollection?.length);
    const commandsInLocalScope = this.SlashCommandArray.filter(command => command.guildCollection).map(command => Object.assign(command, command.data));

    const filterLocalCommands = commandsInLocalScope.filter(local => !existingCommands.some(cache => cache.name === local.name));
    const booleanLocalCommands = this.cacheCommands(filterLocalCommands, false);

    const filterGlobalCommands = globalCommands.filter(global => !existingCommands.some(cache => cache.name === global.name));
    const booleanGlobalCommands = this.cacheCommands(filterGlobalCommands, true);

    if (!(booleanLocalCommands || booleanGlobalCommands)) {
        this.log('There are no commands to load. No changes have been made!', 'cache');
        return;
    }

    if (booleanGlobalCommands) {
        await this.application.commands.set(globalCommands).catch((err) => this.log(err, 'error'));
        this.log('Global scope application commands (/) have been successfully loaded!', 'client');
    }

    if (booleanLocalCommands) {
        for (const guildID of commandsInLocalScope.flatMap(command => command.guildCollection)) {
            const commands = commandsInLocalScope.filter(
                cmd => cmd.guildCollection.includes(guildID)
            );

            const guild = this.guilds.cache.get(guildID);
            if (!guild) {
                const verify = await this.guilds.fetch(guildID)
                this.log(`The server ${verify.name} (${verify.id}) is not in the client's cache`, 'error');
                continue;
            }

            await guild.commands.set(commands).catch((err) => this.log(err, 'error'));
            this.log(`Local scope application commands (/) have been successfully loaded in the guild: ${guild.name} (${guild.id})!`, 'client');
        }
    }
}

# EventMap

All right, now let's take our event class to the next level. It's a straightforward process with room for improvement. Continuing in the Structures folder, let's open our EventMap.js file and update our structure to:

class EventMap {
  constructor(client, options) {
    this.client = client;
    this.name = options.name;
    this.once = options.once || false;
    this.log = this.client.log;
  }
}

export default EventMap;

This way, we've implemented the system for custom logs by calling it from our extended client class.

# PrefixCommands

Now, within our PrefixCommands.js file, we will enhance it for better performance and organization. So, let's update it as follows:

import { PermissionFlagsBits } from 'discord.js';

class PrefixCommands {
  constructor(client, options) {
    this.client = client;
    this.name = options.name;
    this.description = options?.description;
    this.aliases = options?.aliases;
    this.isPrivate = options.isPrivate;
    this.userPermissions = options.userPermissions;
    this.botPermissions = options.botPermissions || [PermissionFlagsBits.SendMessages];
    this.mentionCommand = options?.mentionCommand;
    this.onlyDevs = options.onlyDevs;
    this.guildCollection = options.guildCollection;
    this.log = this.client.log;
  }
}

export default PrefixCommands;

Now, you can refer to the table below for detailed information about the properties of PrefixCommands class:

Property Description Type
name The unique name associated with the command. String
description A description of the command. String
aliases An array of aliases associated with the command. Array
isPrivate A boolean value indicating whether the property is used in direct messages (DMs) or not. Boolean
userPermissions An array of required user permissions for the command. Array
botPermissions An array of required bot permissions for the command, with a default value of SendMessages. Array
mentionCommand A boolean value indicating whether the property serves as a command mention. Boolean
onlyDevs A boolean value indicating whether the property is private and accessible only to developers. Boolean
guildCollection An array representing the collection of servers associated with the command. Array
log A function that represents the client's log object. Function

# SlashCommands

Now, let's enhance our latest SlashCommands class. To do that, navigate to the Structure folder and update it as follows:

import { PermissionFlagsBits } from 'discord.js';

class SlashCommands {
  constructor(client, options) {
    this.client = client;
    this.name = options.name || options.data.name;
    this.description = options.description || options.data.description;
    this.options = options.options || options.data?.options;
    this.userPermissions = options.userPermissions;
    this.botPermissions = options.botPermissions || [PermissionFlagsBits.SendMessages]; 
    this.onlyDevs = options?.onlyDevs || false;
    this.deferReply = options.deferReply || false;
    this.guildCollection = options.guildCollection; 
    this.log = this.client.log;
  }
}

export default SlashCommands;

Now, you have access to the table below, providing in-depth information on the properties of SlashCommands class:

Property Description Type
name The unique name associated with the command. String
description A description of the command. String
options The command's options. String
userPermissions An array of required user permissions for the command. Array
botPermissions An array of required bot permissions for the command, with a default value of SendMessages. Array
mentionCommand A boolean value indicating whether the property serves as a command mention. Boolean
onlyDevs A boolean value indicating whether the property is private and accessible only to developers. Boolean
deferReply A boolean value to delay the reply, often used for interactions with longer processing times. Boolean
guildCollection An array representing the collection of servers associated with the command. Array
log A function that represents the client's log object. Function

# Syncing Events

Now that we've upgraded our structures for better performance, let's enhance our events to boost application efficiency. This module is essential for optimizing our application, as we'll improve and implement systems within the events to synchronize with our structures.

# ready

Now, let's implement our custom logs and the mosaic we created within the ready event. It's a straightforward change, like this:

import EventMap from '../../Structure/EventMap.js';
import terminal from '../../Utils/Functions/createMosaic.js';

export default class extends EventMap {
  constructor(client) {
    super(client, {
      name: 'ready',
      once: true
    });
  }
  run = async () => {
    console.log(terminal)  
      
    await this.client.registerCommands();
    this.log(`O client ${`${this.client.user.tag}`.blue} ${`(${this.client.user.id})`.blue} foi iniciado com êxito!`, 'client');
  };
};

# interactionCreate

Here is where things get truly intriguing. We will sync our interactionCreate with the enhanced SlashCommands class, implementing various functionalities and handling potential future errors. This way, our event will look like this:

import EventMap from '../../Structure/EventMap.js';
import Config from "../../Config/Config.json" assert { type: "json" };

export default class extends EventMap {
  constructor(client) {
    super(client, {
      name: 'interactionCreate'
    });
  }
  run = async (interaction) => {
    const commandName = interaction.commandName;
    const command = this.client.SlashCommandArray.find((c) => c.name === commandName);

    if (interaction.isAutocomplete()) {
      await command.autocomplete(interaction);
    } 
    
    else if (interaction.isChatInputCommand()) {
      let load = { content: `❌ **|** ${interaction.user} couldn't execute this command, it's either invalid or non-existent.`, ephemeral: true }
      if (!command) {
        await interaction.editReply(load).catch(() => interaction.reply(load));
        return;
      }

      if (command.onlyDevs && !Config.default_developers.includes(interaction.user.id)) {
        let dev = { content: `❌ **|** ${interaction.user} this command is private and can only be executed by authorized developers of this application.`, ephemeral: true }

        this.log(`User ${interaction.user.username} (${interaction.user.id}) is not an authorized developer.`, 'notice')
        await interaction.editReply(dev).catch(() => interaction.reply(dev));
        return;
      }

      const noUserPerm = { content: `❌ **|** ${interaction.user} you don't have permission to use this command!`, ephemeral: true }
      const noBotPerm = { content: `❌ **|** ${interaction.user} I don't have permission to execute this command!`, ephemeral: true }

      if (command.botPermissions && !command.botPermissions.some(role => interaction.guild.members.me.permissions.has(role))) {
        this.log(`I lack the permissions to execute the ${commandName} command in the server ${interaction.guild.name} (${interaction.guild.id}).`, 'notice')
        await interaction.editReply(noBotPerm).catch(() => interaction.reply(noBotPerm));
        return;
      } 
      
      else if (command.userPermissions && !command.userPermissions.some(role => interaction.member.permissions.has(role))) {
        this.log(`User ${interaction.user.username} (${interaction.user.id}) doesn't have permission to execute the ${commandName} command.`, 'notice')
        await interaction.editReply(noUserPerm).catch(() => interaction.reply(noUserPerm));
        return;
      }

      if (!this.client.cooldown.has(interaction.user.id)) {
        if (!command) {
          const updateCommand = "❌ **|** Sorry, we couldn't execute this command at the moment. Please try again later or contact support if the issue persists."

          this.log(`An error occurred while executing the ${commandName} command. Please check if the command is up to date.`, 'notice')
          await interaction.editReply(updateCommand).catch(() => interaction.reply({ content: `${updateCommand}`, ephemeral: true }));
          return;
        }

        const time = new Date(new Date().getTime() - (180 * 60 * 1000)); 
        const date = time.toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' });

        this.log(`User ${`${interaction.user.username}`.cyan} ${`(${interaction.user.id})`.cyan} executed the ${`'${commandName}'`.bgMagenta} command at ${`${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`.blue} on ${`${date}`.blue}`, 'notice')

        if (command.deferReply) {
          await interaction.deferReply();
        }

        command.run(interaction);
      } else {
        const cooldown =  `🚫 **|** ${interaction.user} you are in cooldown, please wait for 5 seconds to use commands again.`

        this.log(`User ${interaction.user.username} (${interaction.user.id}) reached cooldown with the ${commandName} command, as they attempted to execute it repeatedly.`, 'notice')
        await interaction.editReply(cooldown).catch(() => interaction.reply({ content: `${cooldown}`, ephemeral: true }));
        return;
      }

      await this.client.cooldown.add(interaction.user.id);
      setTimeout(async () => await this.client.cooldown.delete(interaction.user.id), 5000);
    }
  }
}

Wow, that's a significant change, isn't it? Feel free to explore each property I've added to handle any errors during interactions with the application. The text is just a superficial representation to report what's happening during user actions.

# messageCreate

And finally, in our latest update to the messageCreate event, we'll follow the same approach as in interactionCreate to handle potential errors and enhance the understanding of the structure.

import EventMap from '../../Structure/EventMap.js';
import Config from '../../Config/Config.json' assert { type: "json" };

export default class extends EventMap {
    constructor(client) {
        super(client, {
            name: 'messageCreate'
        });
    }
    run = async (message) => {
        if (message.author.bot) return;

        const time = new Date(new Date().getTime() + (-180 * 60 * 1000));
        const date = time.toLocaleDateString('en-US', { day: '2-digit', month: '2-digit', year: 'numeric' });
 
        const prefix = Config.default_prefix;
        const [pv, ...argsArray] = message.content.trim().split(" ");

        if (!message.guild && this.client.PrefixCommandArray.some(command => command.name.toLowerCase() === pv.toLowerCase())) {
            const command = this.client.PrefixCommandArray.find(cmd => cmd.name.toLowerCase() === pv.toLowerCase());
            if (command.isPrivate && !message.guild) return;
            if (command.onlyDevs && !Config.default_developers.includes(message.author.id)) return;

            this.log(`User ${`${message.author.username}`.cyan} ${`(${message.author.id})`.cyan} executed the command ${`'${command.name}'`.bgMagenta} at ${`${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`.blue} on ${`${date}`.blue}`, 'notice')
            command.run(message, argsArray);
            return;
        }

        const startCommand = message.content.toLowerCase().startsWith(`<@${this.client.user.id}>`);

        if (!(message.content.toLowerCase().startsWith(prefix) || startCommand)) return;

        const content = startCommand ? message.content.slice(`<@${this.client.user.id}>`.length).trim() : message.content.slice(prefix.length).trim();
        const [cmd, ...args] = content.split(" ");
        const command = this.client.PrefixCommandArray.find((c) => c.name === cmd.toLowerCase() || c.aliases?.includes(cmd.toLowerCase()))

        if (!command) return;
        else if (!command?.mentionCommand && startCommand) return;
        else if (command.isPrivate && !message.guild) return;

        else if (command.onlyDevs && !Config.default_developers.includes(message.author.id)) {
            this.log(`User ${message.author.username} (${message.author.id}) is not a set developer.`, 'notice')
            return;
        }

        else if (Array.isArray(command.guildCollection) && !command.guildCollection.includes(message.guild?.id)) return;

        else if (message.guild) {
            if (command.botPermissions && !command.botPermissions.some(role => message.guild.members.me.permissions.has(role))) {
                this.log(`I lack permission to execute the command ${cmd} on the server ${message.guild.name} (${message.guild.id}).`, 'notice')
                message.reply(`❌ **|** ${message.author} I do not have permission to use this command!`).then(m => setTimeout(() => m?.delete(), 5000)).catch(() => { })
                return;
            }

            if (command.userPermissions && !command.userPermissions.some(role => message.member.permissions.has(role))) {
                this.log(`User ${message.author.username} (${message.author.id}) does not have permission to execute the command ${cmd}.`, 'notice')
                message.reply(`❌ **|** ${message.author} you do not have permission to use this command!`).then(m => setTimeout(() => m?.delete(), 5000))
                return;
            }
        }

        else if (!this.client.cooldown.has(message.author.id)) {
            this.log(`User ${`${message.author.username}`.cyan} ${`(${message.author.id})`.cyan} executed the command ${`'${cmd}'`.bgMagenta} at ${`${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}`.blue} on ${`${date}`.blue}`, 'notice')
            command.run(message, args);
        } else {
            this.log(`User ${`${message.author.username}`.cyan} ${`(${message.author.id})`.cyan} hit cooldown with the command ${`'${cmd}'`.bgMagenta}` +
            ` as they attempted to execute it repeatedly.`, 'notice')
            message.reply(`🚫 **|** ${message.author} you are in cooldown, please wait 5 seconds to use commands again.`).then((m) => setTimeout(() => m?.delete(), 5000));
            return;
        }

        await this.client.cooldown.add(message.author.id);
        setTimeout(async () => await this.client.cooldown.delete(message.author.id), 5000);
    }
}
Last Updated: 10/20/2023, 6:32:59 PM