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.'
};