Here at Box UK we often meet up at the end of the week for a pint down at the local pub. Our local changes what beers are on tap daily, so they use a TV to display what beers they have available together with useful information such as the price and type of beer.

Previously I thought these were just images displayed on the TV screen but one day I noticed the TVs were actually rendering a web page!

For internal comms at work we use HipChat, and we have Hubot installed to help us out with various tasks. For those who haven’t met Hubot yet, he’s a Node.js chatroom bot created by GitHub to do all manner of things from telling jokes to deploying code.

The grand plan

This got me thinking. I wondered if we could ask Hubot what beers are on at our local using the HTML page they use on the TV to get that information.

Hubot scripts are written in CoffeeScript and after a bit of wrangling I had my initial concept working. Hubot comes with a HTTP library for obtaining data from an address, a bit like curl. Using it is as easy as:

robot.respond /(what|which) beers are (on|available)/i, (msg) ->
   msg.http('http://www.example.com').get() (err, res, body) ->
       console.log body

This returns the body of the response for me to use; for me this was a load of HTML so I needed to parse this HTML to extract the data out.

I could do this with some funky regular expressions to strip out the tags and grab the data I’m looking for. But instead I used a library called cheerio. Cheerio accepts a string of HTML and uses jQuery like syntax to query it. This was really cool because I already knew jQuery fairly well so it was easy to get started.

Once I had the part of the page I wanted I could iterate over the table rows and get the data from the cells, adding them to a beers array.

$ = cheerio.load(body);
boardRows = $(".beer-board > table tr");
boardRows.each (i, el) ->
    info = [];
    $(this).children().each (i, el) ->
        info.push $(this).text().trim();
   
 beers[i] = "(beer)  " + info.join(", ");

Now that I had the information it was just a case of printing that to the page beer by beer.

msg.send "Hey there!n At the taphouse on tap we have:";
msg.send beers.join("n");
msg.send "Cheers! (beer)"

This worked really well; as you can see I have the beer name, the brewery, the percentage, price and type of beer. All this came from the beerboard!

Hubot Untapped info

Untappd promises

We deployed this to the server and people started to use it, feedback was good but an interesting request that came in was to integrate Untappd ratings for each beer.

At first I added another http request to Untappd inside the boardRows.each loop which parsed Untappd in much the same way as I had done previously with the beer board. Unfortunately I forgot that Node and therefore Hubot runs its code asynchronously. This meant that the original beerboard request wouldn’t wait for the Untappd ones to complete.

I got around this by using javascript promises via the Q library. Essentially a promise says “given I run this code, then do these things afterwards”. My Untappd promise looks a little bit like this:

getRating = (msg, query, info) ->
        deferred = q.defer();
        msg.http('https://untappd.com/search?q=' + query).get() (err, res, body)->
                if (err)
                        deferred.reject() # You broke your promise! :(
                else
                        $ = cheerio.load(body);
                        rating = $('.results-container .beer-item:first-child .num');
 
                       # Format the rating into gold stars
                        rating = rating.text()
                        rating = rating.substring(1, rating.length-1)
 
                       message = '';
                        $i = 1
                        while $i <= Math.round(rating)
                                message += '(goldstar)'
                                $i++
 
                       # Push the rating to the info collection
                        info.push message unless !message
 
                       beers.push '(beer) ' + info.join(', ');
                        deferred.resolve(); # Say the promise was fufilled
 
       return deferred.promise

This meant I could tell the script to wait for all the Untappd responses to finish before sending my response.

q.all(promises).then(() ->
    msg.send "Hey there!nAt the taphouse on tap we have:n" +
 beers.join("n") + "nCheers! (beer)";
)

Finishing touches

To add a bit of character I added some suggested excuses from the devs that would appear in the event that something went wrong. These were picked at random and it adds a nice playful touch to the script.

excuses = [
  'No beers here. Go to the City Arms instead.',
  'Sorry, dunno what's on, we're all drunk.',
  'ZZZzzz...wha..no, drank it all, try somewhere else...zzzZZZzzzZZZzzz...'
]

In the end I had a lot of fun writing this script and it’s proved quite handy, especially when the end of the week rolls around and you’re curious to see what’s available behind the bar!

Hubot beer information

You can find the script on GitHub or as a package on npm.

About the Author

Robert Rhoades

Robert is a Software Engineer with extensive professional experience working with a full spectrum of languages from server- to client-side. During his time at Box UK Robert has worked on a diverse range of projects for various prestigious clients including RS Components and BMJ.