Discord Bot with AI / Voice Recognition
view all blogs...

Justin Bess :: June 1, 2018

Creating a discord bot with AI and speech recognition

Here recently, I had a long time acquaintance invite me over to a Discord server. The server is intended for professional growth, team capture the flag events, and your typical information security related items. I decided that I could spark things up by creating a bot. My initial plan was to have a bot to poll for CVE updates, and relay anything new to the server.

While I did stick with the original plan, I also ended up integrating a few other things into the bot, which I had not initially desired to add. I added voice recognition, artificial intelligence, and audio streaming capabilities; I even have it where the bot can talk! Before I dive too deep, I’ll point out the bot is hosted on my Github repository as concord. If you would like to view any of the code, or download and run your own Discord bot based on mine, simply follow the instructions outlined in the README file.

The reason I chose to develop the bot in Discord.js, is because it has complete support for the entire Discord API, it’s maintained, it’s reliable, and there is no point in inventing the wheel all over again. So the overall goal of this bot was to broadcast new CVE’s, as they were introduced into several platforms, including the national vulnerability database. I found a site, which hosts a nice API, for such things over at circl (computer incident response center Luxembourg). I’m always pro-oop, so with object oriented programming in mind, I built a CVE class, with the necessary functions to establish my desired needs.

Fetching data with the API

The heart of this class will rely on the circl API, so it’s apparent that we are going to need to interact with the API somehow. To do this, I constructed a method which interacts with the service using the Node.js http service.

const config = require('../config')
const FILES = config.cve.FILES

module.exports = class CVE {
  constructor(client){
    this.https = require('https')
    this.apiBaseURL = 'https://cve.circl.lu/api/'
    this.client = client
    this.pending = []
  }

  apiFetch (part, cb) {
    this.https.get(this.apiBaseURL + part, (resp) => {
      let data = '';

      resp.on('data', (buff) => {
        data += buff
      })

      resp.on('end', () => {
        if(cb) cb(this.getJSON(data))
      })

    })
    .on('error', (err) => {
      console.log('An error was encountered while polling:', err.message)
    })
  }

  getJSON (json) {
    try {
      let o = JSON.parse(json);
      if (o && typeof o === "object") return o;
    }
    catch (e) {
      console.log('Error parsing JSON: ', e)
    }
    return false;
  }

}

The part parameter inside the apiFetch() function, will be appended to the API’s base URL. In this case, it’s set in the constructor appropriately as this.apiBaseURL = 'https://cve.circl.lu/api/'. The http constructor property makes use of the Node.js http module. We also allow for the user to pass in a potential callback function to the method, using cb as the parameter name. On lines 20-22, you will notice where we check for the callback, and call it accordingly, if it’s present.

resp.on('end', () => {
  if(cb) cb(this.getJSON(data))
})

The pending property on line 9, set as this.pending = [] will take an array of CVE requests, which will later be used to determine if the API is currently fetching some result, so users can not duplicate queries accidentally, or repeat the query because they feel like it’s not being responsive. The use of getJSON() is self explanatory. It takes some data passed in, and converts it into a JSON string. This is the typical way an API would respond with it’s data, but we can’t be too careful. You wouldn’t want to try parsing something as a JSON object, when the server didn’t even respond with such an object.

Request API calls indefinitely

We want our bot to update us when a new CVE is published. This means that the bot will have to continuously request the latest CVE’s published. We’ll then have to compare the new CVE list, to the last list we acquired. If we notice a different CVE in our list, then we can safely assume that a new CVE has been added. Let’s take a look at how we can implement this functionality.

poll (func, time) {
  setInterval(func.bind(this), 10*1000* /*n minutes*/ time)
}

log (content, file) {
  let fs = require('fs')
  fs.writeFile(file, content, function (err) {
    if (err) return console.log(err)
  })
}

fromFile (file, cb) {
  let fs = require('fs')
  fs.readFile(file, 'utf8', function (err, data) {
    if (err) return console.log(err)
    if (cb) return cb(data)
    return data
  })
}

compare (current, previous) {
  return current === previous
}

latest () {
  let _log = this.log
  let _fromFile = this.fromFile
  let _messageChannel = this.messageChannel.bind(this)

  function success (data) {
    let fs = require('fs')
    if (fs.existsSync(FILES['latest'])) {
      _fromFile(FILES['latest'], (prevData) => {
        prevData = JSON.parse(prevData)
        let newest = data[0]
        let previous = prevData[0]
  
        if ( newest.id !== previous.id ) {
          _messageChannel(`NEW CVE FOUND:: ${newest.id}: ${newest.summary}`)
          _messageChannel(`https://cve.circl.lu/cve/${newest.id}`)
          _log(JSON.stringify(data), FILES['latest'])
        } else {
          console.log('No CVE updates to report')
        }
      })
    } else {
      _messageChannel('Building CVE data!')
      console.log('Creating ' + FILES['latest'])
      _log(JSON.stringify(data), FILES['latest'])
    }
  }

  this.apiFetch('last', (data) => { success(data) } )
}

This purpose of the poll function is to take some desired function, and run it every x minutes. We use the built in setInterval() function in JavaScript for this. We created the compare() function, which will be used to compare the new API result to the previous result. The results of each API call is grabbed with the latest() function, and is then logged into a log file set in the config settings, and is written to via the log() function. We created a function to read the old request logs, and named it accordingly as fromFile(). With all of this together, we have the functionality needed to request for published CVE’s every x minutes, and compare it to a list of old results, and distinguish new CVE’s from the most recent list!

Creating the command class

I have checked out several IRC bots in the past, outside of Discord.js, and I’m all too familiar with seeing functions hard-coded directly in the core area of the bot, after it’s established a connection and authenticated with a server. I think this approach leads to bloated code that’s not very maintainable. I built an IRC bot written in Ruby in the past, and used the approach just mentioned. I took some time to think about what I was doing, and came up with something that I believe to be a viable solution to the problem.

class Loader {
  constructor() {
    this.methods = []
  }

  add (method) {
    this.methods.push(method)
  }

  execute (name, props) {
    this.methods.map(meth => {
      if (name === meth.name) {
        meth.cb(props)
      }
    })
  }

  isMethod (name) {
    let results = this.methods.filter((meth) => {
      return meth.name === name
    })
    return results.length < 1 ? false : true
  }
}
const methodManager = new Loader()
module.exports = methodManager

This is a very simple class, but it solves a great problem with our maintainability and bloating issue described earlier. The class will use an Array to store all defined commands to be used by the bot. Each command will simply be an Object, with properties that structure how the bot should use the command. The command object will then be pushed into the Loader array using the add() method. We can check if a command exists within the loader by using isMethod() function. This function will be useful in the main loop when checking if the user supplies a command. The execute() command finds the correct command from the list of commands, and executes the function defined as cb inside the command object, and passes any additional parameters as the props parameter.
The command object can have properties to include things like auth, locked, cb, name Where auth could set an authentication level such as moderators only or admin, the locked property is something that could be set by an admin or moderator to disable a command, name could be what to call to execute the function, and cb is the function that will be called. Let's look at how the core of the bot makes use of the loader class.

const methodManager = require('./loader')

client.on('message', msg => {
  let parts = msg.content.split(' ')
  let cmd = parts[0]
  let options = parts.slice(1, parts.length).join(' ')

  if (methodManager.isMethod(cmd)) {
    let props = {client, msg, parts, options, cve}
    methodManager.execute(cmd, props)
  }

})

As you can see, we require the loader class in the first line as methodManager Whenever we receive a message, we break up the text into an array separated by spaces, and use the first word as our command. We set options as everything after the command. Then we use the isMethod() method from our loader class to check if the command is in the command list, and if it is, we run the execute command with the built up options. Now this is all fancy, but we haven't loaded our methods or shown how to create a command, so let's talk about that next.

Creating a command

All commands will be created inside the /functions directory, and separated into individual files. I recommend using a filename that's similar to your command name, so it's easier to recognize when you need to edit files. Let's create a command named say

const methodManager = require('../loader')

function say (props) {
  // get what the user said through the props.parts property
  let content = props.parts.slice(1, props.parts.length).join(' ')

  // send our reply to the message, pulled from the msg property
  props.msg.reply(content)
}
let sayMethod = {
  name: '!say',
  locked: false,
  help: '!say ',
  cb: repeat
}
methodManager.add(sayMethod)

As you can see, we loaded in the loader class as methodManager. We know our props that are being passed in from the core of the application, so we can use this to reply to the correct channel, and with the correct data. We have not locked the method by default, and we added a helper for the method.

Loading our commands to the loader class

I decided to separate my commands into a directory called functions, and load them in with a simple loop using fs.

function loadFunctions () {
  let fs = require('fs')
  let methods = fs.readdirSync('./functions/');
  for (let i in methods) {
    let file = methods[i].split('.')[0]
    require('./functions/' + file)
  }
}

// Initialize functions
loadFunctions()

Comments