I loved coding through that exercise but soon, I found myself on a dead-end: I finished and refactored my solution and I still wanted more. What could I do next? I set myself to find learning mates to do pair programming with, using chat-builder itself, transforming a dead-end into a side project, curious to see where that will take me. The plan: build a js robot that will listen and reply in the chat, capturing peers attention as they test their own solutions, pushing them to contact me on twitter or via email.
The first version, as a proof of concept, was pretty basic. It was mainly a fork of my chat-builder solution with some DOM manipulation stripped off and renamed methods. Everything was hardcoded into the robot single js file program. It was definitely hard to extend. Some early concepts are still presents in the latest version, like normalize now found in the adapter, or the print method, a reminiscence of the chat-builder UI. That’s also why Hackbot live in the browser, while Hubot live on the server.
Initially the Bot(later renamed Robot) object had a listen() method used to fetch messages from the parse API. listen was invoked passing brain as callback.
The main elements of the Hackbot (and Hubot) architecture are:
Robot, Adapter, Listener, Message & Response and scripts.
#### Robot ##### What is it and why we need it Robots receive messages from a chat source, and dispatch them to matching listeners. Robot is the central part of our application. Among other things it has a name (default to ‘Hackbot’), a brain as storage system, an adapter to interact with a specific chat source (in our case the chat-builder API), a collection of listeners to analyse incoming messages and few other utilities.
How to create a robot
Hackbot says: Bite my shiny metal ass!
Loading scripts at ../hackbot/scripts/
Under the hood
The created robot object will looks like:
Through its prototype chain a robot has access to the following methods:
#### Adapter ##### What is it and why we need it An adapter is a specific interface to a chat source for robots. In the case of Hackbot its adapter is made to work with chat-builder as chat source. Shortly, adapters can be seen as some kind of middleware API, sitting between the robot and the chat source.
Under the hood
The adapter through its prototype chain has access to the following methods (you will notice that few of them have similar names to the methods we listed above for the robot itself. This is because in some cases the robot will just delegate the action to its adapter):
What is it and why we need it
Listeners receive messages from the chat source and decide if they want to act on them. Among other things, they have the following properties: a regular expression that determines if the listener should trigger the callback, the callback itself (a function to be triggered if the incoming message matches) and through its prototype chain it has access to a call method that determines if the listener likes the content of the message. If so, a Response built from the given Message is passed to the Listener’s callback.
Under the hood
Shortly, when robots receive a message (via an adapter), will invoke the listener via its call method passing the message as argument:
Inside that call function, if the listener’s regex match the message text, the listener’s callback function will be invoked passing a new response as argument:
See annotated documentation of the listener for more details
#### Message & Response ##### What are them and why we need them Messages and responses are data structures used to carry data around the application. Messages get created inside the adapter with data receive from the chat source. Responses get created inside listeners when Messages trigger a listener’s callback, as we just saw earlier.
Under the hood
A message object, has the following properties:
Through its prototype chain a message has access to a match method to determines if the message matches a given regex, and is used by the listener’s matcher method when invoked trough its call method as we saw above. The Response object wrap the message and contain some useful properties, it look something like:
Through the prototype chain, the response object has access to the following methods: send, emote, reply, random and finish.
Check the full annotated documentation of Message and Response for further details.
##### What are them and why we need them Scripts refer to the system that allow to extend Hackbot. In other words they are little plugins that extend the functionality of the bot. Scripts are made with the goal of adding one or more listeners to our robot. For that reason they will need to define what the robot will listen for (using a regular expression), and what will the robot do if it hears the right words (using a callback function).
How to create listeners within scripts
There are two methods available within a robot for that: hear() and respond(). Both accept a regex and a callback function as parameters. They differ in how the robot will listen to messages. With hear(), robot listen to the whole conversation, checking all words, while with respond(), robots will listen to words only when somebody calls it. For example:
User: ‘Oh, nice to be in here, hello world’
User: ‘@Hackbot: hey bot, hello ’
User: ‘Hackbot please say hello to me’
robot.hear(/hello/, callback) will trigger its callback in all the three examples above, while robot.respond(/hello/, callback) only in the latest two.
Similarly, if in the callback we want our Robot to say something in the chat, we have three options: using response.send(), response.reply() and response.emote(). They will generate the following type of messages in the chat:
Hackbot: ‘Thank you all ’ // send()
Hackbot: ‘@User: Thank you Sir’ // reply()
Hackbot: ‘* is Happy *’ // emote()
The latest useful methods to be aware when scripting Hackbot are response.finish() and response.random().
Response.finish() will mark the message as done (by setting the value of his property done to true). The result of this is that the message won’t be passed to any other listener for checking after that moment.
Response.random() is a simple method that return a random value out of an array. Can be useful to create random replies or sentences. For an example, see how its being used in the thank you script.
How to add help commands to a script
To add some help to a script that can be called by typing the help command in the chat, use the help method:
If in the chat somebody type help, robot will respond as follow:
Hackbot: < Hello > — Make Hackbot saying Hello
See the annotated documentation of the help script for more details. In Hackbot the help system is implemented as a script, so you can disable help by not loading that script. *** ### Hackbot vs Hubot Although Hackbot mimic very closely Hubot’s architecture, the two are different in some aspects.
Browser vs Server
Of course, being built around chatbuilder, Hacknot require chatbuilder.js. Although this could be relegated to a dependency for the adapter only, the chatbuilder.js has been added as a global dependency to be inline with the scope of the exercise. As a nice side effect, there is no need to add jQuery as it comes packed within chatbuilde.js itself.
In order to take advantage of some functional utilities similar to what the underscore.js library offer, I’ve created a very little library called underscorish. At the moment underscorish come packed with very limited methods:
If you want to take advantage of the underscorish.js library when creating your own scripts, just define the relative dependency following the require.js syntax as follow:
For more details, please check the underscorish annotated documentation.
#### Brain Hackbot runs in the browser, so gaining writing access to local file system in order to have a persistent storage was anything but trivial. After digging possible solutions it seemed that using the html5 localStorage API was the best things to do. Hackbot’s brain therefore differ from the Hubot brain implementation as it use the localStorage API. Few extra methods are then available to deal with it:
See annotated documentation of the brain for more details
Similar to brain, I had to find a way for Hackbot to know what scripts to load. In order to achieve this, two things has been slightly changed. First the load() method for the robot:
We saw this earlier when I showed how to create a robot. Path is a string (i.e: ‘/newScriptFolder’). If nothing is passed to the method, the default will be ‘../hackbot/scripts/’ as found in the github repository structure. It’s important to know that in the defined directory a file named scripts.js must be present. In that file, all scripts that Hackbot should load must be listed and the corresponding files must exist in the same directory. Then it’s just a matter of calling the load() method:
See annotated documentation of the scripts for more details
For the adapter few things were different compared to other Hubot adapters. First of all the chat-builder API automatically generate dummy messages for the sake of the challenge. That’s why the Hackbot adapter come with a private helper clean() in order to remove dummy content from the fetched data. Another important private helper added to the Hackbot adapter is normalize(). As the adapter pull data from the chat-builder API at regular intervals, the normalize() helper implements some kind of temporary cache to be sure that the same message is not sent twice to the robot and its listeners.
See annotated documentation of the adapter for more details.
To test and use Hackbot I’ve developed few scripts, some have been ported from Hubot while others have been developed from scratch. Treat them as examples to learn and understand how to program Hackbot. Below is an high level overview of some of them.
This little script log every user message on screen, using the print method.
This script take advantage of the robot brain storing all the users messages. This allow for retrieveing both the user list and the message history for a single user. To use it type:
< Hackbot chat users > — list all users who chatted in here
< Hackbot hackbot chat history [name] > — show all messages from [name] — case sensitive
< Hackbot chat history me > — show all my messages
This script can be used to leave a message to somebody that is not online now and deliver it once the received is back (when he/she writes something in the chat). This script have been ported from a Hubot script. To use it type:
< Hackbot ambush [name] : message > — leave a message for [name]
This script uses 3rd part APIs. Those APIs have been created using kimonolabs and they fetch the following data from hackreactor.com:
- Next program dates.
- Names, github, linkedin, personal sites, and twitter handles of the previous alumni.
To use it type:
< Hackbot hr program > — show next courses as published on hackreactor.com/program
< Hackbot hr students (name | twitter | github | linkedin | site) > — take and list relative students data from hackreactor.com/students
For more details check the full documentation:
Conclusion and Future
Documentation and code
Hackbot uses inline comments formatted accordingly to docco.js. Please check the whole Hackbot documentation to learn more. Feel free to fork Hackbot and send a pull-request on Github.
Thanks for reading, if you have any questions, please get in touch on twitter or drop me a line at nick at balestra dot ch.