Making Meekee, a Slack bot using Google Calendar

Meekee is a tiny slackbot coded in Node.js that taps into the Google Calendar API and sends notifications when there’s have an event coming up.

As a distributed team, we had a lot of small frustrations around joining remote calls on a daily basis. We made Meekee to help ease the pains of finding the hangout link/the room, remembering what the meeting is about, when it starts, etc. Meekee sends a ping 3 mins before it starts so you can keep working until then but still have enough time to be in the right mindset when it starts.

This article is geared towards the tech side of things. Check out Tomomi Sasaki’s excellent article first to get an idea of why we built it. 😉

You might want to read this article if :

  • you are wondering how other people structure their architecture and especially software without a UI, like a bot.
  • you are curious about how to work with GoogleCalendar Api in Node.js, renewing tokens over time and handling auth callback outside the browser.

There are lots of tutorials existing online already about how to make bots so I won’t get into that. Slack’s Easy-Peasy-Bot code is a good place to start.

The Stack

  • Written in Node.js
  • Botkit to help with bot functionalities (user install, listening, talking, etc.).
  • MongoDB for storage using botkit-storage-mongo, a driver for Botkit.
  • GoogleCalendar API with google-api-nodejs-client lib to watch events.
  • Hosted on Heroku with Codeship for continuous integration.

The code is open source if you wanna take a peek: github.com/aqworks/meekee-bot

This is a rough overview of the main process:

The main setup

First, Botkit loads index.js, connection to the database and Slack are initiated. Botkit will callback onInstallation for new teams installs.

var controller = app.configure(process.env.PORT, process.env.SLACK_CLIENT_ID,
 process.env.SLACK_CLIENT_SECRET, config, onInstallation);
controller.on('rtm_open', function (bot) {
 if (!userLib)
   userLib = require('./lib/user').init(controller);
   controller.webserver.get('/google/auth/callback', function(req, res) {
   res.send("<p>Next, copy and paste this Authentication Code in Slack to @meekee: <br/>
   <strong>"+req.query.code+"</strong></p>");
 });
setTimeout(function(){
   loop(bot, true);
 }, 3000);
 setInterval(function(){
   loop(bot, false);
 }, 60000);
}

controller.on("rtm_open") is called for each different team to which connection succeeded. So it’s like there is a thread for each team. UserLib is instantiated here because it contains a context specific to teams.

There is also a bit of a hack to handle the GoogleApi authentication callback. I’ll come back to this. ☝️

Finally, a timer to execute the loop function each minute to check events send and notifications. More on that later too.

Interactions with the bot

Meekee responds to simple communication but it’s not a chatbot so it only processes simple keywords. It didn’t feel necessary to have a bot with natural language processing ability since the core features are event notifications.

Meekee understand start and stop as “turn On/Off notifications”.

Botkit provides methods to listen and react to certain keywords:

controller.hears(["hello", "hi", "greetings"], ["direct_mention", "mention", "direct_message"],
 function(bot,message) {
   bot.reply(message, "Hello! :simple_smile:");
 }
);

The install process

When a new team installs the bot, Botkit takes care of the installation and connection to slack.

Meekee then handles the extra step: asking users for access to their Calendar:

As I mentioned in the intro, it was a little tricky to figure out how to handle callback from Google Calendar API. The normal auth process will ping the endpoint you provided with an auth code.

Meekee doesn’t “live” in the browser though. There is an endpoint setup to listen for this code (the controller.webserver.get('/google/auth/callback') in controller.rtm(on_open) ) so the code is being retrieved. But because the context is not shared between the browser and the bot, there is no way to know which user the code belongs to… The user is asked to paste it back to Slack. Then the auth is finalized.

This was a part I tried to tackle from many angles. I ended up settling for this inelegant way of handling things. Meekee was originally meant as an internal tool so that was good enough.

A side note about Google Calendar API tokens

Meekee establishs connection to Google Calendar once. It would be irritating if access had to be granted again every week or if notifications were missed because the token had expired, right? 😅 Here’s how to make the token ‘sticky’.

oauth2Client.generateAuthUrl({
 access_type: 'offline',
 approval_prompt: 'force',
 scope: [ 'https://www.googleapis.com/auth/calendar.readonly' ]
});

generateAuthUrl returns the url that will be sent the user for authentication to google API. The access_type: 'offline' and approval_prompt: 'force' parameters are used to enable the refresh_token . In this mode, Gcal will send a fresh token when the previous one expires. ✨ At each query to the calendar, updateToken() confirms the Google token is still valid or updates the token in database if it has been refreshed.

The loop

Side note: Heroku restarts the server instance every 24H so on the first run, meekee checks the user timezones, updates if necessary. People at AQ travel a lot and they like their notifications in the right timezone!

The core of meekee is this loop checking each user’s events every minute.

checkCalendar is called to :

  • Query the google Calendar API for the current user,
  • Filter events,
  • Format notifications,
  • Update the database (keep count of notifications sent),
  • Callback to the loop and send notifications.

checkCalendar = function(user_object, callback){
 var notifications = [];
 var error = null;
 updateToken(user_object);
 var args = calendar.getEventArgs(oauth2Client);
 google.calendar('v3').events.list(args, function(err, res){
   if (err)
     error = calendar.handleQueryError(err, args);
   else
     notifications = calendar.filterEvents(args, res, user_object);
   if (notifications.length){
     user_object.notifications += notifications.length;
     saveUser(user_object, function(){
       if (err)
           console.error(err);
       }, false);
     }
   }
   callback(user_object, notifications, error);
 });
}

Anatomy of the query to the Calendar Events endpoint:

  • auth contains the identification to Gcal like user token, client Id, client secret, and also redirect url for the callback.
  • calendarId: primary Only the main calendar is being watched at the moment.
  • maxResults: 5 Assuming not more than 5 events are happening at once.
  • singleEvents: true Includes recurring events regular events.
  • orderBy: startTime Event starting first, shows up first in the results.
  • timeZone: utc Events are treated on UTC timezone. User timezone is applied when formatting notifications.
  • timeMin: now+3mins,timeMax: now+4mins Select all events starting in 3 minutes or currently happening (including ‘All day’ events). Events not actually starting at that time will be filtered out.

Unless en error happens, this query will return a set of events.

Filter out the events and format notifications

The call tocalendar.filterEvents then filters out events that fit these criteria and format as notifications:

  • Events that actually starts in 3 to 4 minutes.
  • Events to which other persons except the user are invited (and answered yes or maybe). Meekee doesn’t notify of event that involves no one else.

Send notifications!

Back in the loop, through the callback, send the notification. Slack provides a lot of options to format messages with columns, images and such.

A random emoji everytime ❤︎

Meekee also has 3 onboarding checkpoints to help each user along the way. The loop is used to send those as well.

2 hours: has the user granted access to google Calendar? If not, let’s remind him.

1 week: User is encouraged to give feedback to our twitter account @aqbots and to let their coworkers know about the bot. I’m cheating here, I’ve been using using meekee for much longer than a month.

4 weeks: Meekee sends a little message to thank them, let them know how many notifications they got so far and who is using the bot on their team.

What I learned

I learned that a ‘tiny bot’ can still be a lot of work especially if it is of a time sensitive nature, is sitting between 2 APIs and has multiple users and teams.

It can be tricky to pin point a bug and troubleshoot in time and context due to the abundance of async processes. Classic logging is not of much help here. I did not implemented full fledged tests because, as I said it’s internal tool and it would have been quite tricky mocking the APIs. But, it has proven to me once more than writing tests is never a waste of time. It will almost always save you some time later. And that especially when it is tricky to validate your code manually.

Auth is complicated. Despite existing standards, it can be hard to figure out how to establish a connection and maintain it over time. Small projects can be a great opportunity to contribute to Open Source.

I needed functions that did not exist in botkit-storage-mongo. So I coded them and made a small Pull Request that was merged in the code source later. It does need to be a big change. If you need it, changes are someone else might too.

That’s it

Thanks for reading this post. I hope you found some value in it.

You can find meekee on meekee.io, @aqbots on twitter and browse the source on Github meekee-bot.

You can reach out to me on Twitter @oiorain.

October 24, 2017