cyclone-engine
v1.4.3
Published
A stable Discord bot engine/framework centered around granting features and automation without hindering developer freedom
Downloads
19
Maintainers
Readme
An advanced bot engine for Discord running on lightweight Eris
What can Cyclone do?
Manage and automate connections to the Discord API
Handle commands with capability, versatility, and ease
Add user flexibility to your bot with command aliases
Prevent crashing due to errors
Integrate automated actions
Simplify how attachments such as databases are integrated into systems
Auto generate command info
Utilize a dynamic built-in help menu generator
Allow freedom of server-side prefixes
Return command results for analysis and logging
Create interactive menus with awaited actions and reactions
Grant complete freedom of bot design
Assign authority levels to server roles with an ALO permissions system
Examples of bots that use Cyclone
Getting started
Prerequisites
eris
- You need to install Eris and supply it to the agent. Eris is supplied manually to allow custom Eris classes to be used by the engine.
Documentation
npm i cyclone-engine
Constructing the Agent class
The Agent class is the main manager of the bot. This will be controlling automated actions as well as call the Command & Reaction Handler.
const {
TOKEN
} = process.env
const Eris = require('eris')
const {
Agent
} = require('cyclone-engine')
const handlerData = require('./data/')
function postFunction (msg, results) {
if (results) console.log(`${msg.timestamp} - **${msg.author.username}** > *${results.command.name}*`)
}
const agent = new Agent({
Eris,
token: TOKEN,
handlerData,
options: {
connectRetryLimit: 5,
prefix: '.',
postEventFunctions: {
message: postFunction,
reaction: postFunction
}
}
})
agent.connect()
Using an attachment such as a database manager
If you'd like to have a class/object/value that can be utilized by commands even if it's not defined in the same scope, you can use the attachment feature
Knex example:
Main file:
const {
TOKEN,
DATABASE_URL
} = process.env
const Eris = require('eris')
const Knex = require('knex')
const {
commands
} = require('./data/')
const knex = new Knex({
client: 'pg',
connection: DATABASE_URL
})
const agent = new Agent({
Eris,
token: TOKEN,
handlerData: {
commands
}
})
agent.attach('db', knex)
agent.connect()
Command file:
const {
Command
} = require('cyclone-engine')
const data = {
name: 'points',
desc: 'See how many points you have',
action: ({ agent, msg }) => {
return agent.attachments.db('users')
.select('points')
.where({
id: msg.author.id
})
.limit(1)
.then((res) => {
if (res) return res.points
else return 'You aren\'t registered in the database'
})
}
}
module.exports = new Command(data)
Constructing the Command Handler without the agent
The Command Handler is taken care of automatically when the agent is constructed and connected. However, if you would not like to use the agent, you can construct the handler separately.
const {
TOKEN
} = process.env
const Eris = require('eris')
const client = new Eris(TOKEN)
const {
CommandHandler
} = require('cyclone-engine')
const {
commands,
replacers
} = require('./data/')
const handler = client.getOAuthApplication().then((app) => {
client.connect()
return new CommandHandler({
client,
ownerID: app.owner.id,
commands,
replacers
})
})
client.on('messageCreate', (msg) => handler.handle(msg))
Creating Commands
The Command Handler takes an array of command and replacer classes to function. A multifile system is optimal. A way to implement this would be a folder containing JS files of every command with an index.js
that would require every command (Looping on an fs.readdir()
) and return an array containing them.
Command File:
const {
Command
} = require('cyclone-engine')
const data = {
name: 'say',
desc: 'Make the bot say something.',
options: {
args: [{ name: 'content', mand: true }],
restricted: true /* Make this command bot-owner only */
},
action: ({ args: [content] }) => content /* The command returns the content provided by the user */
}
module.exports = new Command(data)
Awaiting Messages
Certain commands require multiple messages from a user. If a command asks a question, it will usually want to await a response from the user. This can be done with awaits.
Command File:
const {
Command,
Await
} = require('cylcone-engine')
const data = {
name: 'ban',
desc: 'Ban a user',
options: {
args: [{ name: 'username', mand: true }]
},
action: ({ client, msg, args: [username] }) => {
const user = client.users.find((u) => u.username.toLowerCase() === username.toLowerCase())
if (!user) return '`Could not find user.`'
const rspData = new Await({
options: {
args: [{ name: 'response', mand: true }],
timeout: 10000,
onCancelFunction: () => msg.channel.createMessage('Ban cancelled.').catch((ignore) => ignore)
},
action: ({ args: [response] }) => {
if (response.toLowerCase() === 'yes') {
return client.banMember(user.id, 0, 'Banned by: ' + msg.author.username)
.then(() => 'User banned')
.catch(() => '`Bot does not have permissions.`')
} else return 'Ban cancelled.'
}
})
return {
content: `Are you sure you want to ban `${user.username}`? (Cancels in 10 seconds)`,
awaits: rspData
}
}
}
module.exports = new Command(data)
Creating Replacers
Replacers are passed to the command handler and are applied to messages that trigger commands. Using keywords, live data can be inserted into your message as if you typed it. For example, you could replace |TIME|
in a message with the current date and time.
Replacer File:
const {
Replacer
} = require('cyclone-engine')
const data = {
key: 'TIME',
desc: 'The current time',
options: {
args: [{ name: 'timezone' }]
},
action: ({ args: [timezone] }) => new Date(new Date().toLocaleString('en-US', { timeZone: timezone })).toLocaleString()
} /* If I wrote `!say |TIME America/New_York|` at 12:00PM in London on Frebruary 2nd 1996, The bot would respond with `2/2/1996, 7:00:00 AM`. (The timezone is optional)*/
module.exports = new Replacer(data)
Constructing the Reaction Handler without the agent
The Reaction Handler is taken care of automatically when the agent is constructed and connected. However, if you would not like to use the agent, you can construct the handler separately.
const {
ReactionHandler
} = require('cyclone-engine')
const {
reactCommands
} = require('./data/')
const handler = client.getOAuthApplication().then((app) => {
client.connect()
return new ReactionHandler({
client,
ownerID: app.owner.id,
reactCommands
})
})
client.on('messageReactionAdd', async (msg, emoji, userID) => handler.handle(msg, emoji, userID))
Creating React Commands
React commands listen for when any user reacts to any command with a certain emoji.
React Command File:
const {
ReactCommand
} = require('cyclone-engine')
const {
MODERATOR_CHANNELID
} = process.env
const data = {
emoji: '❗', /* A custom emoji would be `:name:id` (Animated emojis are `a:name:id`) */
desc: 'Report a message to the moderators',
action: ({ msg, user }) => {
return {
content: `Reported by *${user.username}*. Message link: https://discordapp.com/channels/${msg.channel.guild.id}/${msg.channel.id}/${msg.id}`,
embed: {
author: {
name: msg.author.username,
icon_url: msg.author.avatarURL
},
title: msg.content
},
options: {
channels: MODERATOR_CHANNELID
}
}
}
}
module.exports = new ReactCommand(data)
Binding interfaces to messages
Interfaces are a group of emojis the bot adds to a messages. When an emoji is clicked, the bot executes the appropriate action. Interfaces can be bound manually with ReactionHandler.prototype.bindInterface()
See documentation, or they can be included in the options of an action return (This includes commands, awaits, and react commands).
const {
Command,
ReactInterface
} = require('cyclone-engine')
const {
ADMIN_ROLEID,
MUTED_ROLEID
}
const data = {
name: 'manage',
desc: 'Open an administrative control panel for a user',
options: {
args: [{ name: 'username', mand: true }]
},
action: ({ client, msg, args: [username] }) => {
if (!msg.member.roles.includes(ADMIN_ROLEID)) return '`You are not authorized.`'
const user = msg.channel.guild.members.find((u) => u.username.toLowerCase() === username.toLowerCase())
if (!user) return '`Could not find user.`'
const muteButton = user.roles.includes(MUTED_ROLEID)
? new ReactCommand({
emoji '😮', /* Unmute */
action: () => {
return user.removeRole(MUTED_ROLEID, 'Unmuted by: ' + msg.author.username)
.then(() => 'User unmuted')
.catch(() => 'Missing permissions')
}
})
: new ReactCommand({
emoji: '🤐', /* Mute */
action: () => {
return user.addRole(MUTED_ROLEID, 'Muted by: ' + msg.author.username)
.then(() => 'User muted')
.catch(() => 'Missing permissions')
}
})
const msgInterface = new ReactInterface({
buttons: [
muteButton,
new ReactCommand({
emoji: '👢', /* Kick */
action: () => user.kick('Kicked by: ' + msg.author.username).catch(() => 'Missing permissions')
}),
new ReactCommand({
emoji: '🔨', /* Ban */
action: () => user.ban('Banned by: ' + msg.author.username).catch(() => 'Missing permissions')
})
],
options: {
deleteAfterUse: true
}
})
return {
content: `**${user.username}#${user.descriminator}**`,
options: {
reactInterface: msgInterface
}
}
}
}
module.exports = new Command(data)