Nutritious Pumpkin Meal

    cyclone-engine

    1.4.3 • Public • Published
    Splash Banner

    Quality Assurance Test Coverage Maintainability

    Discord Server

    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

    • 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)

    Design sparked by Alex Taxiera

    Install

    npm i cyclone-engine

    DownloadsWeekly Downloads

    1

    Version

    1.4.3

    License

    MIT

    Unpacked Size

    122 kB

    Total Files

    21

    Last publish

    Collaborators

    • exorift