Development

Slack, Bots, Coffeescript, DoD, and SMS

Meet Rambot, our randomizer bot!

June 16 2016

Like every developer team working in Scrum, our Team OneAPI has a clear 'definition of done' (DoD) for each task, so someone else can try out a new feature, review code, etc. So much so that we even have a dedicated page on Confluence.

Now, the concept of that someone else is the most difficult part of this process. You must find that "lucky" person. Why you try, suddenly everyone is busy, in a rush or turns deaf. So, we had to randomize it.

First iteration - Groovy script

...was quite trivial:

def list = Arrays.asList(Member.values())
Collections.shuffle(list)
print list.iterator().next()
 
public enum Member {
    MATS,
    DENIS,
    PETAR,
    MATO
}
                

…but was done in matter of seconds. You had to randomize and assign the task on JIRA. The main problem is that's easy to compromise. As nobody likes to do DoD tasks, nobody will trust you.

Second iteration - bookmarklet

...was cool but still didn't solve the problem:

javascript:(function () {
 
    function random(){
        var devs = ["Matija Bruncic", "Denis Cutic", "Matija Matosevic"];
        return devs[Math.floor(Math.random()*devs.length)]
    }
 
    var assignee = random();
    document.querySelectorAll("[data-field-text='"+assignee+"']")[0].setAttribute("selected", "selected");
 
    document.getElementById("assign-issue-submit").click()
})();

You had to go to that JIRA task in your browser and run bookmarklet. Still, nobody will trust you. And (same as the first iteration) the code had to differ for each member. Plus, it probably doesn't work on IE.

Third iteration - Slackbot

...was very easy. And it solved the problem of biasing the randomness.

random_person_d_o_d

Slack has a default bot - Slackbot which can answer one of the predefined responses on predefined requests. But it’s very, very annoying when he picks you!

slack_default_bot

Final iteration - Hubot

...is the coolest

One of the reasons we adopted Slack is its easy integration with bots, and opportunity to write some cool and useful code. We decided to use Slack-Hubot. It has Hubot under the hood, it's easy to setup and program. I won't go into too many details, but you can find the documentation online.

slackbot_create_ma_a_friend

And you must name it. Our bot is tough guy, so we named him Rambot. You program your logic in coffeescript, and it's like you're writing nodejs app. For example, you can list network interfaces:

module.exports = (robot) ->
   robot.hear /rambot, where are you?/i, (res) ->
     os = require('os');
     ifaces = os.networkInterfaces();
     Object.keys(ifaces).forEach (ifname) ->
       ifaces[ifname].forEach (iface) ->
         if 'IPv4' != iface.family or iface.internal != false
           # skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
           return
       # this interface has only one ipv4 adress
         res.send 'here somewhere: ' + ifname, iface.address
         return
       return

But let's return to the subject at hand!

We've created a simple script which recognizes the user who sent the request and randomly picks one of the remaining team members:

users = ["denis.cutic", "josip", "mmatosevic", "mbruncic", "pducic"]
module.exports = (robot) ->
  robot.hear /dod/i, (response) ->
    possible = (user for user in users when user != response.message.user.name)
    lucky = possible[Math.floor(Math.random() * possible.length)]
    response.send "Hi #{response.message.user.name}! Your dod is assigned to #{lucky}"

But then we could not stop programming, so we added a feature which enable Rambot to update task assignees through JIRA API:

jiraData = JSON.stringify({
  fields: {
    assignee: {
      name: luckyJiraUsername
    }
  }
})
response.http("https://jira.infobip.com/rest/api/2/issue/#{task}")
  .auth(jiraUsername, jiraPass)
  .header('Content-Type', 'application/json')
  .put(jiraData) (err, res, body) ->
    response.send(body)

And of course, an Infobip SMS API powered feature to notify that lucky guy with an SMS:

ibData = JSON.stringify({
  to:users[lucky].gsm,
  text:"You're the lucky one: #{task}"
})
response.http("https://api.infobip.com/sms/1/text/single")
  .auth(infobipUsername, infobipPass)
  .header('Content-Type', 'application/json')
  .post(ibData) (err, res, body) ->
    response.send "sent SMS!"

The final code looks something like this:

jiraUsername = process.env.HUBOT_JIRA_USERNAME
jiraPass = process.env.HUBOT_JIRA_PASS
infobipUsername = process.env.HUBOT_IB_USERNAME
infobipPass = process.env.HUBOT_IB_PASS
 
users = {
  "denis.cutic": {
    jira: "dcutic",
    gsm: "3859********"
  },
  josip: {
    jira: "jantolis",
    gsm: "3859********"
  },
  mmatosevic: {
    jira: "mmatosevic",
    gsm: "3859********"
  },
  mbruncic: {
    jira: "mbruncic",
    gsm: "3859********"
  },
  pducic: {
    jira: "pducic",
    gsm: "3859********"
  },
}
 
module.exports = (robot) ->
  robot.hear /dod (.*)/i, (response) ->
    task = response.match[1]
    possible = (user for user, value of users when user != response.message.user.name)
    lucky = possible[Math.floor(Math.random() * possible.length)]
    luckyJiraUsername = users[lucky].jira
    if jiraUsername? && jiraPass?
      jiraData = JSON.stringify({
        fields: {
          assignee: {
            name: luckyJiraUsername
          }
        }
      })
      response.http("https://jira.infobip.com/rest/api/2/issue/#{task}")
        .auth(jiraUsername, jiraPass)
        .header('Content-Type', 'application/json')
        .put(jiraData) (err, res, body) ->
          response.send(body)
 
    if infobipUsername? && infobipPass?
      ibData = JSON.stringify({
        to:users[lucky].gsm,
        text:"You're the lucky one: #{task}"
      })
      response.http("https://api.infobip.com/sms/1/text/single")
        .auth(infobipUsername, infobipPass)
        .header('Content-Type', 'application/json')
        .post(ibData) (err, res, body) ->
          response.send "sent SMS!"
 
    response.send "Hi #{response.message.user.name}! Your dod is assigned to #{lucky} "

From the user's perspective it looks like this:

rambot_assigned_a_person.png

That's it! If you have some improvement ideas, we do accept pull requests. The entire code is available on GitHub.

Enjoy!

By Petar Ducic, Software Engineer / Team Leader