Email iconShapeUntitled 2Untitled 2ozFill 166crosscupcake-iconPage 1HamburgerPage 1Page 1Oval 1Page 1Email iconphone iconpushpinblog icon copy 2 + Bitmap Copy 2Fill 1medal copy 3Group 7twitter iconPage 1

Introduction

Botty McBotface was born as a hack day project not long ago. Botty’s original job was to remind everyone via Slack when it’s time for stand up each day and randomly pick a member to ‘walk’ the kanban wall. If the member isn’t present we can reply with a negative response and she’ll pick another member from the team.

bot-big

I joined UVD on the placement scheme and within the first couple of weeks, Kirsten suggested that I could improve Botty so that team members are able to manage their holidays through Slack using Botty. Kirsten, Alex, VJ and I sat down to decide what Botty must be do and created user stories for each of the desired functionalities. We decided that I’d deliver these features incrementally, starting with a small feature like ‘finding out my remaining holiday days’ and working up to more complex fautures such as ‘booking multiple days of holidays’ which was the last card in the backlog at the time. I added these features to Botty’s existing code in Node.js.

I also created an API for Botty using Symfony. The reason why the API was developed in a different framework was to familiarise myself with the Symfony components, as at UVD we use Symfony for most of our backend work. I used BDD (Behaviour-Driven Development) and TDD (Test-Driven Development) to develop the API.

Botty McBotface – holiday booker

Botty’s original code was written in Node.js using Botkit. Botkit deals with the basic communication with Slack’s API and simplifies the building of conversional bots. Their exhaustive documentation makes this even easier. I’ve yet to discover any other framework as good as Botkit.

To get started, we created a separate channel on Slack for managing holidays, so days off are transparent for the whole team.

Folder structure

Before I show you any examples of what Botty can do, I think it would be a good idea to talk a little bit about the folder structure.

  • index.js – interacts with Slack.
  • Modules
    • Entity – contains representation of objects involved such as User
    • Provider – has files which return number responses to send dates in various formats etc
    • Repository – send requests to holiday manager API and returns the response
    • Validator – contains Date, response and holiday validators which check correctness of information received

The index file is the ear and mouth of Botty, it contains methods that carry out conversations with users. It will retrieve messages that match the regex provided and start a conversation.
The modules folder is the brains of botty, this is where most of the logic is stored. You can see above the folders within modules folder and what they do.

Helping users

dsf
One of the first things I added was a function to help users. If anyone says ‘help’, Botty will list a few things we can ask her. The functions listed by Botty (above) are only available in the holiday channel. Botty will list different things depending on which channel you ask for help in. This is done using a simple function to check which channel the message is sent in, this generates a channel-appropriate help message.

var Botkit = require('botkit');
var MessageProvider = require('./modules/Provider/MessageProvider');

var controller = Botkit.slackbot({
  debug: false
  //include "log: false" to disable logging
  //or a "logLevel" integer from 0 to 7 to adjust logging verbosity
});

// connect the bot to a stream of messages
controller.spawn({
  token: <my_slack_bot_token>,
}).startRTM();

controller.hears(['.*(hello|help|hey|hi).*'], 'direct_mention,mention', function (bot, slackMessage) {

	...

    if (isHolidayChannel(slackMessage)) {

        helpMessage = MessageProvider.getHolidayChannelHelpMessage();

    }

	...

    bot.replyWithTyping(slackMessage, helpMessage);

});

Botkit makes it so easy to receive incoming messages and send responses. There’s in-depth documentation about everything Botkit offers on their GitHub page. You can see above how easy it is to get started. The above controller will intercept any message sent to Botty with a direct mention (“@botty help”) or an indirect mention (“hello @botty”) and matches the provided regex (".*(hello|help|hey|hi).*"). Then I construct and send the help message after checking which channel it is sent in.

How many days holiday do I have left?

Screen Shot 2016-12-19 at 16.24.44

The first functionality I worked on was to find out the number of holidays remaining for a user. Here you can see it follows similar structure to the help message function but the logic for getting a response changes.

controller.hears(['(remain|have|left|holiday).*(remain|have|left|holiday)'], 'direct_mention,mention', function (bot, slackMessage) {

    if (!isHolidayChannel(slackMessage)) {
        return;
    }

To get a user’s number of holidays remaining I start by checking if the message was asked in holiday channel or not. This is because we decided to manage our holidays in a separate Slack channel.

    let user = new User(slackMessage);

    if (!user.isValidUserMention()) {
        return bot.replyWithTyping(slackMessage, MessageProvider.userNotFound());
    }

I create a new user object if the message is sent in the correct channel and check for a valid user if someone mentions another person. This is important as we decided that any UVD member should be able to manage other users’ holidays on their behalf.

    if(user.isAdmin()){
        return bot.replyWithTyping(slackMessage, MessageProvider.getAdminHolidayMessage(user));
    }

If the target user is not an admin and is part of UVD, then I send the API request to the holiday manager API and Botty replies with an appropriate response, whether it’s their remaining holidays or an error message.

controller.hears(['(remain|have|left|holiday).*(remain|have|left|holiday)'], 'direct_mention,mention', function (bot, slackMessage) {
 
    if (!isHolidayChannel(slackMessage)) {
        return;
    }

    let user = new User(slackMessage);
 
    if (!user.isValidUserMention()) {
        return bot.replyWithTyping(slackMessage, MessageProvider.userNotFound());
    }
    
    if(user.isAdmin()){
        return bot.replyWithTyping(slackMessage, MessageProvider.getAdminHolidayMessage(user));
    }

    const targetSlackID = user.getSlackId();
 
    holidayRepo.getHolidaysRemaining(targetSlackID).then((res) => {

        const holidaysRemaining = res.data.data.holidays_remaining;
        const holidaysEndPeriodDate = res.data.data.holiday_period_end;

        bot.replyWithTyping(slackMessage, MessageProvider.holidaysRemainingSuccess(user, holidaysRemaining, holidaysEndPeriodDate));

    }).catch((err) => {bot.replyWithTyping(slackMessage, MessageProvider.holidaysErr(user, err));});
 });

Booking a holiday

A more complicated example would be the holiday booking process. In our first attempt Botty would ask for the holiday start date from the user and confirm it, then she would ask for the end date, and confirm it. Then ask for a summary and send a acknowledgment message then send a request to the API with these parameters to book the holiday.

Botty holiday booking process

After a feedback session with the team, it became apparent that this was quite long winded; more so if people wanted to book multiple holidays, as they’d have to go through this process every time. Therefore we decided to have a fast track holiday booking process. 

Fast track holiday booking

This new holiday booking process would require the holiday start and end date from a single message and confirm it, then Botty would ask for the holiday summary and book the holiday. This was quite useful because users didn’t have to go through the long process to book their holidays.

controller.hears(['BOOK_HOLIDAY_REGEX'], 'direct_mention,mention', function (bot, slackMessage) {
    ...

    const INVALID_DATE_ERR_CODE = DateValidator.getErrCodeIfDatesInvalid(slackMessage.text);

    if (INVALID_DATE_ERR_CODE){
        bot.replyWithTyping(slackMessage, MessageProvider.invalidDateMessage(INVALID_DATE_ERR_CODE));
        return;
    }

    var holidayDates = DateProvider.getHolidayDates(slackMessage.text);


Similar to the other functions, holiday booking too starts by checking if the message was sent in the correct channel or not, then it checks for a valid mention, followed by creating a user object. I then validate the dates provided by the user using regex, if they’re invalid Botty replies with an error message and the conversation ends, otherwise Botty will start a new conversation to confirm the dates provided. which is shown below.

    confirmDates = function(question, holidayDates, convoResponse, convo) {

        convo.ask(question,function(slackUserResponse,convo) {

            if (QUIT_REGEX.test(slackUserResponse.text)) {
                bot.replyWithTyping(slackUserResponse, MessageProvider.cancelledRequest());
                return convo.next(); //return early
            }

            let yes = YES_REGEX.test(slackUserResponse.text);
            let no = NO_REGEX.test(slackUserResponse.text);

            if (yes && !no) {
                getHolidaySummary(holidayDates[0], slackUserResponse, convo);
            } else if (no && !yes) {
                bot.replyWithTyping(slackUserResponse, MessageProvider.tryAgainMessage());
                return convo.next();;
            } else {
                convo.say(MessageProvider.unknownUserConfirmationResponse());
                confirmDates(question, holidayDates, slackUserResponse, convo);
            }
            convo.next();
        });
    }

At this point users can reply with a positive or a negative response, replying with anything else will cause Botty to repeat the confirm message until you quit, confirm it or cancel your request. I have used two types of responses, bot.replyWithTyping and convo.say. replyWithTyping slows down the response making Botty more human like, where convo.say is used for quick response and Botty will reply instantly.

    let getHolidaySummary = function(holidayDates, convoResponse, convo) {

        let question = MessageProvider.holidaySummaryQuestion(user);

        convo.ask(question,function(slackUserResponse,convo) {
            //Return early if slackUserResponse matches quit regex
            ...

            bot.replyWithTyping(slackUserResponse, MessageProvider.BookingHolidayAckMessage());

            holidayRepo.bookHoliday(holidayDates, slackUserResponse).then((res) => {

                bot.replyWithTyping(slackUserResponse, MessageProvider.bookHolidaySuccess(user, res));
            }).catch((err) => {bot.replyWithTyping(slackUserResponse, MessageProvider.holidaysErr(user, err));});

            convo.next();
        });
    }

    let question = MessageProvider.holidayDatesConfirmationQuestion(user);

    bot.startConversation(slackMessage, (convoResponse, convo) => confirmDates(question, holidayDates, convoResponse, convo));
});

Should a user confirm their dates, Botty will ask them to provide a holiday summary by asking questions similar to ‘are you going anywhere nice’ or ‘what are you getting up to?’. There’s no validation on the holiday summary so users are free to enter anything they wish. 

After getting the holiday summary, Botty will send an acknowledgement message followed by a request to the holiday manager API using the dates provided by the user.

Upon receiving a response from the API, Botty will either confirm the holiday booking or reply with an error message generated from the API error code.

Summary

You saw what Botty is able to do, but there’s a lot of room for improvement. We had a retrospective to let the team say what they like about Botty and what they think we can improve on.
I received a lot of good feedback and we’ve lined up plenty of improvements and functionalities we wish to add to Botty, end each feature is an opportunity to use TDD to refactor Botty’s code and improve it based on what I have learnt. Some of the features
include booking holidays in past, special features that only admin can access, and adding a bank of responses for Botty to choose from.

Share: