JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 7
  • Score
    100M100P100Q37054F
  • License MIT

An easy to use Discord bot client manager/engine and event handler combo! Automate everything with a stable bot framework.

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

Splash Banner

CircleCI codecov Discord Server Version Node Version NPM Downloads

NPM Page

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.

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,
  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. *