literallyobjects

An easy-to-use Discord bot framework that heavily depends on object literals to express command structures and metadata.

Usage no npm install needed!

<script type="module">
  import literallyobjects from 'https://cdn.skypack.dev/literallyobjects';
</script>

README

LiterallyObjects

LiterallyObjects is a Discord bot framework that heavily depends on JavaScript object literals to express command structures and metadata.

In-depth documentation will be provided soon, so here's some boilerplate code for now:

A 'Hello world' snippet of code:

const fs = require('fs');
const Client = require('literallyobjects');

const client = new Client({
    // Your basic discord.js ClientOptions
    messageCacheMaxSize: 50,
    messageCacheLifetime: 240,
    messageSweepInterval: 360,
    disabledEvents: ['TYPING_START'],
    disableEveryone: true
}, {
    // Configuration file
    config: require('./config.json'),

    // Language stuff
    language: {
        perms: require('./language/perms.json'),
        presences: require('./language/plays.json'),
        pasta: require('./language/pasta.json')
    },

    // The short-hand categories of your commands
    categories: {
        info: 'Help & Information',
        util: 'Utility',
        mod: 'Moderation',
        fun: 'Fun',
        img: 'String & Image Manipulation',
        config: 'Configuration',
        maint: 'Maintenance'
    },

    // The path for the database used - see LukeChilds' `keyv` package on npm for more information on this one.
    // If you actually use this example, remember to create a `data` folder and install https://github.com/recapitalverb/keyv-sqlite through npm!
    databasePath: 'sqlite://./data/database.sqlite'
});

client.on('warn', console.warn);
client.on('error', console.error);

// Login
client.login(client.config.token)
    .catch(console.error);

Loading commands:

// Bind the client object and Discord to global scope so commands can access it
Object.assign(global, {
    client,
    Discord: require('discord.js')
});

// Load all command files under the `commands` dir
fs.readdir('./commands/', (err, files) => {
    if (err) return console.error(err);
    files.map(file => {
        if (!file.endsWith('.js')) return;
        let props = require(`./commands/${file}`);
        let commandName = file.split('.')[0];
        client.commands.set(commandName, props);
    });
    client.help.build();
});

// Load all events files under the `events` dir
fs.readdir('./events/', (err, files) => {
    if (err) return console.error(err);
    files.map(file => {
        if (!file.endsWith('.js')) return;
        const event = require(`./events/${file}`);
        let eventName = file.split('.')[0];
        client.on(eventName, event);
    });
});

Editable and deletable command editing needs extra event functions to be added.

// events/message.js
module.exports = async function (msg) {
    // Handle the message
    client.handler(msg, function (content, options, traces) {
        // Reply to the user
        client.util.done(msg, content, options)
            .then(function (m) {
            // If the reply is successful, we add the output along with other info to the binds map
                if (m) {
                   client.binds.set(msg.author.id, {
                       input: msg.id,
                       output: m,
                       traces,
                       timer: client.setTimeout(function () {
                           client.binds.delete(msg.author.id);
                       }, client.config.maxEditTime) // This removes itself from the map to limit the max time between the command execution and the next command edit / deletion
                   });
                }
            });
    }, function (content, options, traces) {
        client.util.throw(msg, content, options)
            .then(function (m) {
                if (m) {
                   client.binds.set(msg.author.id, {
                       input: msg.id,
                       output: m,
                       traces,
                       timer: client.setTimeout(function () {
                           client.binds.delete(msg.author.id);
                       }, client.config.maxEditTime)
                   });
                }
            });
    });
};
// events/messageDelete.js
module.exports = function (msg) {
    let bind = client.binds.get(msg.author.id);
    // Check if the deleted message is recorded in the binds map
    if (bind && bind.input === msg.id) {
        // Delete the bind from the map and clear its timeout
        client.binds.delete(msg.author.id);
        client.clearTimeout(bind.timer);
        // Delete the output
        bind.output.delete().catch(() => undefined);
        // Remove all traces of the previous run
        if (bind.traces) bind.traces.map(function (toDelete) {
            if (toDelete.deletable) toDelete.delete().catch(() => undefined);
        });
    }
};
// events/messageUpdate.js
module.exports = function (old, msg) {
    let bind = client.binds.get(msg.author.id);
    // Check if the edited message is recorded in the binds map
    if (bind && bind.input === old.id && (bind.output.attachments.size === 0)) {
        client.binds.delete(msg.author.id);
        client.clearTimeout(bind.timer);
        if (bind.traces) bind.traces.map(function (toDelete) {
            if (toDelete.deletable) toDelete.delete().catch(() => undefined);
        });
        // We use Promise.all to wait until the client is done removing all its reactions from the message
        Promise.all(
            // Get the message reactions,
            msg.reactions.filter(function (r) {
                // filter and get only the ones the client has reacted to,
                return r.me;
            }).map(function (r) {
                // and remove them from the message, returning a resolved Promise regardless if the client did it or not
                return r.users.remove().then(function () {
                   return Promise.resolve();
                }).catch(function () {
                   return Promise.resolve();
                });
            })
        ).then(function () {
            // After all of that, we handle the new command, edit our previous response to the new response and react to the response if needed
            client.handler(msg, function (content = '', options = {}, traces) {
                // Remove previous content / embed
                if (!options.embed) options.embed = null;
                if (options.files || (options instanceof Discord.MessageAttachment) || (Array.isArray(options) && (options[0] instanceof Discord.MessageAttachment))) {
                   client.util.done(msg, content, options)
                       .then(function (m) {
                           // If the reply is successful, we add the output along with other info to the binds map
                           if (m) {
                               client.binds.set(msg.author.id, {
                                  input: msg.id,
                                  output: m,
                                  traces,
                                  timer: client.setTimeout(function () {
                                      client.binds.delete(msg.author.id);
                                  }, client.config.maxEditTime) // This removes itself from the map to limit the max time between the command execution and the next command edit / deletion
                               });
                           }
                       });
                   return;
                }
                bind.output.edit(content, options).then(function () {
                   client.binds.set(msg.author.id, {
                       input: msg.id,
                       output: bind.output,
                       traces,
                       timer: client.setTimeout(function () {
                           client.binds.delete(msg.author.id);
                       }, client.config.maxEditTime)
                   });
                });
            }, function (content = '', options = {}, traces) {
                msg.react('❌').catch(() => undefined);
                // Remove previous content / embed
                if (!options.embed) options.embed = null;
                if (options.files || (options instanceof Discord.MessageAttachment) || (Array.isArray(options) && (options[0] instanceof Discord.MessageAttachment))) {
                   client.util.throw(msg, content, options)
                       .then(function (m) {
                           if (m) {
                               client.binds.set(msg.author.id, {
                                  input: msg.id,
                                  output: m,
                                  traces,
                                  timer: client.setTimeout(function () {
                                      client.binds.delete(msg.author.id);
                                  }, client.config.maxEditTime)
                               });
                           }
                       });
                   return;
                }
                bind.output.edit(content, options).then(function () {
                   client.binds.set(msg.author.id, {
                       input: msg.id,
                       output: bind.output,
                       traces,
                       timer: client.setTimeout(function () {
                           client.binds.delete(msg.author.id);
                       }, client.config.maxEditTime)
                   });
                });
            });
        });
    } else client.emit('message', msg);
};

An example of a command file named perm.js, used for the above example. Remember to create a folder named commands and move all your command files under it.

// commands/perm.js
module.exports = {
    // The `run` function is called when the user calls the command and all conditions are satisfied (permissions, channel properties, ect.)
    run: async function (msg, p) {
        let member;
        if (p[0]) {
            member = await client.util.getMemberStrict(msg, p[0]);
            if (!member) throw new client.UserInputError('Invalid user provided. Check your spelling and try again.');
        } else member = msg.member;
        return {
            content: `All permissions for **${member.user.tag}** and their states: \n\`\`\`md\n${Object.keys(client.lang.perms).map(function (perm) {
                return (msg.channel.permissionsFor(member).has(perm, false) ? '#  TRUE | ' : '> FALSE | ') + client.lang.perms[perm];
            }).join('\n')}\`\`\``
        };
    },

    // `args` will only be used in a `help` command so that users can understand how the command functions
    args: ['mention/user ID'],
    // This number is used by the command handler to see if the user has provided enough arguments for a command
    argCount: 0,

    // The command's category
    cat: 'util',

    // Whether the command needs to be executed in a guild
    reqGuild: 'true',

    // The command's description
    desc: 'Shows all user permissions, or you can pass a mention/user ID to see that member\'s permissions.'
};