Hacking Hubot with Hubot
Right before last week’s Dallas Ruby Brigade hack night, my coworker Aaron planted an idea in my head. He thought programming Hubot on the fly would be neat. I agreed with him; modifying Hubot from within the chat room simply by talking to him would be awesome. Since I spent last week hacking on Node.js and working on other Hubot scripts like bang-bang, I figured I’d take a crack at it.
Adding functionality to Hubot is easy with Hubot scripts. There are lots of good examples in that repository, but at their core they’re all the same. They take a pattern and a callback. When someone says something that matches the pattern, it runs the callback.
Give that a whirl and you’ll end up with something like:
Piece of cake, right? Turns out, dynamically programming Hubot isn’t much harder. All you need is a regular expression that looks for both a pattern and callback, then evaluates them and calls
robot.respond with the results.
(This seems like as good a time as any to point out that having a programmable Hubot means that it’s possible for someone to write a malicious script. Consider yourself warned.)
The only thing to watch out for here is that this forces the pattern to be case-insensitive by requiring the
i flag. This allows you to address your Hubot as “Hubot”, “hubot”, or “HUBOT”.
But that’s all you need to have a completely programmable Hubot! Here’s how it ends up working:
coffee-script dependency and doing
CoffeeScript.eval instead of the plain
eval, but I didn’t want to do that. Mostly because typing CoffeeScript in chat clients is annoying.
Secondly, there’s a lot of boilerplate. Since we’re
evaling the callback, it needs to be an expression that evaluates to a function. The easiest way to do that is to assign a function to a variable. The obvious choice for a variable name is
_ because we don’t care what it’s called. And we have to pick the parameter name even though most Hubot scripts use
Fortunately CoffeeScript’s string interpolation makes it easy to remove a lot of the boilerplate:
Now it’s much easier to modify Hubot on the fly:
|That pretty much covers the “create” part of CRUD. But what if you want to list all the responders you’ve added to Hubot? Or change one of them? Or remove one of them entirely? At this point, you can’t. Adding that functionality poses more of a challenge and requires diving into Hubot’s internals —||the
Given a regular expression and a callback, it performs some work on the regex before pushing a new listener onto its stack of listeners. That means every time you call
robot.respond, the listeners array grows by one and the last element is the thing you just added. You can use its index to read it back later, modify it, or remove it.
Moving all this functionality into a class made sense to me. I’ll get to the actual implementation in a second, but here’s how the script will end up looking:
As you can see, it’s a pretty simple interface. I called it
Responders so it wouldn’t clash with Hubot’s
Listener class. Using the pattern as a key into the
responder method seemed like a natural choice since I assume you don’t want Hubot to have multiple responses to the same pattern. Everything behaves like you’d expect:
add adds responders,
remove removes them,
responder finds one, and
responders gets all of them.
Now that you’ve seen how it behaves, how does it look behind the scenes? It stores everything as an object in the robot brain, which persists if your Hubot is set up that way.
Adding a responder works by first removing any responder with the same pattern, then adding it with
robot.respond, and finally saving it to the brain. Note that it doesn’t use the
evaled pattern or callback for storage in the brain; this makes it easier to inspect and reason about.
Removing responders is the last piece of the puzzle. First it makes sure it’s actually responding to the pattern in the first place. Then another sanity check to ensure it knows where it is in the listeners array. Then it replaces itself with
(->), an empty callback. After all that, it deletes itself from the brain.
You may be wondering why it assigns an empty callback to the listener instead of deleting it outright. If you delete it, Hubot will complain:
ERROR Unable to call the listener: TypeError: Cannot call method 'call' of undefined.
[Originally posted to my blog.]
Taylor Fausak was born in California, but he got to Texas as soon as he could. He studied Computer Science at the University of Texas at Austin before entering the wild world of software development. After a brief stint at Cisco, he started his career at Famigo working on all aspects of web development. Then he swapped his Django experience for a chunky slice of Rails bacon and joined OrgSync in the fall of 2012. When he's not slinging code around, he likes riding bikes, playing Magic, and throwing frisbees.
comments powered by Disqus