Package Exports
- cyclone-engine
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (cyclone-engine) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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
Stop your bot from crashing due to errors
Integrate automated actions
Simplify how your database is integrated into your systems
Auto generate command info
Utilize a built-in help menu
Get command results for analysis and logging
Create interactive menus with awaited actions and reactions
Complete freedom of bot design
Examples of bots that use Cyclone
Getting started
Prerequisites
eris
- You need to install Eris and supply it to the agent. Eris is supplied to allow custom Eris classes to be used by the engine.
pg, mysql, sqlite, etc.
- In order for the database wrapper, simple-knex
, to function, the database driver you are using must be installed.
dblapi.js
- If you plan on integrating the Discord Bot Labs API into your bot, make sure to have this installed.
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,
DBL_TOKEN,
DATABASE_URL
} = process.env
const Eris = require('eris')
const {
Agent
} = require('cyclone-engine')
const agentData = require('./data')
const agent = new Agent({
Eris,
token: TOKEN,
chData: agentData,
databaseOptions: {
connectionURL: DATABASE_URL,
client: 'pg',
tables: [{
name: 'users',
columns: [{
name: 'score',
type: 'integer',
default: 0
}]
}],
clearEmptyRows: ['users']
},
agentOptions: {
connectRetryLimit: 5,
prefix: '.',
dblToken: DBL_TOKEN,
loopFunction: (agent) => {
agent._client.getDMChannel(agent._CommandHandler.ownerId).then((channel) =>
channel.createMessage('Current server count is: ' + agent._client.guilds.size)
)
}, /* DM the number of guilds the bot is in to the owner */
loopInterval: 1800000, /* 30 minutes */
postMessageFunction: (msg, { command }) => console.log(`${msg.timestamp} - **${msg.author.username}** > *${command.name}*`),
postReactionFunction: (msg, { reactCommand }) => console.log(`${msg.timestamp} - **${msg.author.username}** > *${reactCommand.name}*`)
}
})
Agent
The main controlling agent of the bot.
Parameter | Type | Description | Default |
---|---|---|---|
data.Eris | Eris | The Eris class the system runs off of. | X |
data.token | String | The token to log in to the Discord API with. | X |
[data.handlerData] | Object | The commands and replacers the bot will respond to | {} |
[data.handlerData.commands] | Array<Command> | The commands for the bot. | * |
[data.handlerData.replacers] | Array<Replacer> | The replacers for the bot. | * |
[data.handlerData.reactCommands] | Array<ReactCommand> | The commands that trigger on reactions. | * |
[data.handlerData.replacerBraces] | Object | The braces that invoke a replacer. | * |
[data.handlerData.replacerBraces.open] | String | The opening brace. | '|' |
[data.handlerData.replacerBraces.close] | String | The closing brace. | * |
[data.databaseOptions] | Object | The info for the database the bot utilizes. | {} |
data.databaseOptions.connectionURL | String | The URL for connecting to the bot's database. | X |
data.databaseOptions.client | String | The database driver being used. | X |
[data.databaseOptions.tables] | Array<Object> | The initial tables to set up for the database. | [] |
[data.databaseOptions.clearDefaultRows] | Array<String> | The list of tables to have their unchanged from default rows cleared. | [] |
[data.agentOptions] | Object | Options for the agent. | {} |
[data.agentOptions.connectRetryLimit] | Number | How many times the agent will attempt to establish a connection with Discord before giving up. | 10 |
[data.agentOptions.prefix] | String | The prefix for bot commands. | '!' |
[data.agentOptions.statusMessage] | Object|statusMessageFunction | The status for the bot. It can be an object containing the data, or a callback function for each shard. By default, it's the bot's prefix. | * |
[data.agentOptions.dblToken] | String | The token used to connect to the Discord Bot Labs API. | * |
[data.agentOptions.loopFunction] | Object | A function that will run every loopInterval amount of ms, supplied the agent. | {} |
[data.agentOptions.loopFunction.func] | function | The function. | * |
[data.agentOptions.loopFunction.interval] | Number | The interval at which the loopFunction runs. | 30000 |
[data.agentOptions.fireOnEdit] | Boolean | Whether the command handler is called when a command is edited or not. | * |
[data.agentOptions.fireOnReactionRemove] | Boolean | Whether the reaction handler is triggered on the removal of reactions as well. | * |
[data.agentOptions.postMessageFunction] | postMessageFunction | A function that runs after every message whether it triggers a command or not. | * |
[data.agentOptions.postReactionFunction] | postReactionFunction | A function that runs after every reaction whether it triggers a react command or not. | * |
[data.agentOptions.maxInterfaces] | Number | The maximum amount of reaction interfaces cached before they start getting deleted. | 1500 |
data | Object | The agent data. | X |
[data.agentOptions.userBlacklist] | Array<String> | An array of user IDs to be blacklisted from using the bot. | [] |
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 handler = client.getOAuthApplication().then((app) => {
return new _CommandHandler({
client,
ownerID: app.owner.id,
...require('./data')
})
})
client.on('messageCreate', async (msg) => {
await handler
handler.handle(msg)
})
CommandHandler
The module that handles incoming commands.
Parameter | Type | Description | Default |
---|---|---|---|
[data.agent] | Agent | The agent managing the bot. | {} |
[data.prefix] | String | The prefix of commands. | '!' |
data.client | Eris.Client | The Eris client. | X |
data.ownerID | String | The ID of the bot owner. | X |
[data.knex] | QueryBuilder | The simple-knex query builder. | * |
[data.commands] | Array<Command>|Command | Array of commands to load initially. | [] |
[data.replacers] | Array<Replacer>|Replacer | Array of the message content replacers to load initially. | [] |
[data.options] | Object | Additional options for the command handler. | {} |
[data.options.replacerBraces] | Object | The braces that invoke a replacer. | {} |
[data.options.replacerBraces.open] | String | The opening brace. | '|' |
[data.options.replacerBraces.close] | String | The closing brace. | '|' |
data | Object | The command handler data. | X |
[data.options.ignoreCodes] | Array<Number> | The Discord error codes to ignore. | [] |
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 admin only */
},
action: ({ args: [content] }) => content /* The command returns the content provided by the user */
}
module.exports = new Command(data)
Command
Class representing a command.
Parameter | Type | Description | Default |
---|---|---|---|
data.name | String | The command name. | X |
data.desc | String | The command description. | X |
[data.options] | Object | The command options. | {} |
[data.options.args] | Array<Argument> | The arguments for the command. | [] |
[data.options.aliases] | Array<String>|String | Other names that trigger the command. | [] |
data.options.dbTable | String | The name of database table to fetch user data from (primary key must be named id ). |
X |
[data.options.restricted] | Boolean | Whether or not this command is restricted to admin only. | * |
data | Object | The command data. | X |
data.action | commandAction | The command action. | X |
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.`'
return {
content: `Are you sure you want to ban `${user.username}`? (Cancels in 10 seconds)`,
wait: 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.'
}
})
}
}
}
module.exports = new Command(data)
Await
A class used for the awaiting of a criteria-matching message.
Parameter | Type | Description | Default |
---|---|---|---|
[data.options] | Object | The options for the await | {} |
[data.options.args] | Array<Argument> | The arguments for the await. | [] |
[data.options.check] | checkFunction | The condition to be met for the await to trigger. | () => true |
[data.options.timeout] | Number | How long until the await expires. | 15000 |
[data.options.oneTime] | Boolean | Whether a non-triggering message cancels the await. | * |
[data.options.refreshOnUse] | Boolean | Whether the timeout for the await refreshes after a use. | * |
[data.options.onCancelFunction] | function | A function to run once the await expires or is cancelled. | * |
[data.options.channel] | String | The ID of the channel to await the message. (By default, it's the channel the command was called in.) | * |
data | Object | The await data. | X |
data.action | awaitAction | The await action. | X |
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)
Replacer
A class used to register keywords in a message that are replaced with live data.
Parameter | Type | Description | Default |
---|---|---|---|
data.key | String | The key that invokes the replacer. | X |
data.desc | String | The description of the replacer. | X |
[data.options] | Object | The options for the replacer. | {} |
[data.options.args] | Array<Argument> | The arguments for the replacer. | [] |
data | Object | The data to make a replacer with. | X |
data.action | replacerAction | A function returning the string to replace with. | X |
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 handler = client.getOAuthApplication().then((app) => {
return new _ReactionHandler({
client,
ownerID: app.owner.id,
...require('./data')
})
})
client.on('messageReactionAdd', async (msg, emoji, userID) => {
await handler
handler.handle(msg, emoji, userID)
})
ReactionHandler
The module that handles incoming reactions.
Parameter | Type | Description | Default |
---|---|---|---|
[data.agent] | Agent | The agent managing the bot. | * |
data.client | Eris.Client | The Eris client. | X |
data.ownerID | String | The ID of the bot owner. | X |
[data.knex] | QueryBuilder | The simple-knex query builder. | * |
[data.reactCommands] | Array<ReactCommand>|ReactCommand | rray of reaction commands to load initially. | [] |
[data.options] | Object | Options for the reaction handler. | {} |
[data.options.maxInterfaces] | Number | The maximum amount of interfaces cached before they start getting deleted. | 1500 |
data | Object | The reaction handler data. | X |
[data.options.ignoreCodes] | Array<Number> | The Discord error codes to ignore. | [] |
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: {
channel: MODERATOR_CHANNELID
}
}
}
}
module.exports = new ReactCommand(data)
ReactCommand
A class used to register commands for the command handler.
Parameter | Type | Description | Default |
---|---|---|---|
data.emoji | String | The emoji that triggers the command. | X |
data.desc | String | The description of the react command. | X |
data.options | Object | Additional options for the react command | X |
[data.options.restricted] | Boolean | Whether the react command is restricted to selected users or not. | * |
[data.options.designatedUsers] | Array<String>|String | The IDs of the users who can use the react command. By default, if restricted is true, it's the owner of the message reacted on. | * |
[data.options.dbTable] | String | Name of database table to fetch user data from (primary key must be named id ). |
* |
[data.options.removeReaction] | Boolean | Whether the triggering reaction is removed after executed or not. | * |
data | Object | The react command data. | X |
data.action | reactCommandAction | The react command action. | X |
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')
}
})
return {
content: `**${user.username}#${user.descriminator}**`,
options: {
reactInterface: 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
}
})
}
}
}
}
module.exports = new Command(data)
ReactInterface
An array of emoji button that attach to a message to do different actions.
Parameter | Type | Description | Default |
---|---|---|---|
data.buttons | Array<ReactCommand>|ReactCommand | The buttons of the interface. | X |
[data.options] | Object | The options for the interface. | {} |
[data.options.restricted] | Boolean | Whether all buttons of the interface are restricted to selected users or not. | * |
[data.options.designatedUsers] | Array<String>|String | The IDs of the users who can use the react interface. By default, if restricted is true, it's the owner of the message reacted on. | * |
[data.options.dbTable] | String | Name of database table to fetch user data from (primary key must be named id ). |
* |
[data.options.deleteAfterUse] | Boolean | Whether the interface is deleted after a use or not. | * |
data | Object | The react interface data. | X |
[data.options.removeReaction] | Boolean | Whether the triggering reaction is removed after executed or not. | * |