npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cyclone-engine

v1.4.3

Published

A stable Discord bot engine/framework centered around granting features and automation without hindering developer freedom

Downloads

59

Readme

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