Development

Slack, Bot, Coffeescript, DoD ed SMS

Ecco a voi Rambot, il nostro bot di randomizzazione

June 28 2016

Come qualsiasi team di sviluppatori che lavora in Scrum, anche il Team OneAPI ha una chiara "Definition of done" (DoD) per ciascuna attività così che qualcun altro possa provare una nuova funzione, rivedere un codice ecc., fino al punto di avere anche una pagina dedicata su Confluence.

Trovare quel "qualcun altro", tuttavia, non è sempre facile. Al contrario, scegliere il "fortunato" può rivelarsi un'impresa ardua: al momento di indicare una persona, tutti sono improvvisamente impegnati o di fretta, o semplicemente fanno orecchie da mercante. Così abbiamo dovuto fare in modo che la scelta diventasse casuale.

PRIMA ITERAZIONE - SCRIPT GROOVY

...abbastanza banale:

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

...ma è stata completata in pochi secondi. In questa iterazione era necessario randomizzare e assegnare l'attività su JIRA. Essendo però semplice da compromettere, e dato che a nessuno piacciono le attività DoD, abbiamo visto una certa... mancanza di fiducia.

SECONDA ITERAZIONE - BOOKMARKLET

...non male, ma il problema era ancora lì:

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()
})();

In questo caso era necessario andare sull'attività JIRA nel browser ed eseguire bookmarklet. Anche così, tuttavia, nessuno si fidava, e anche in questo caso era necessario che il codice fosse diverso per ogni membro. Ah, e probabilmente non avrebbe funzionato su IE.

TERZA ITERAZIONE - SLACKBOT

...una possibilità molto semplice, che ha risolto i problemi di fiducia della randomizzazione.

random_person_d_o_d

Slack ha Slackbot, un bot di default in grado di fornire una risposta predefinita a una domanda predefinita. Ma quando ti sceglie, se non fosse un bot vorresti strozzarlo!

slack_default_bot

ITERAZIONE FINALE - HUBOT

...di gran lunga la migliore.

Uno dei motivi per cui abbiamo adottato Slack è la sua integrazione snella con i bot, e il fatto che rendesse possibile scrivere codici utili e interessanti. Abbiamo così deciso di usare Slack-Hubot. All'interno nasconde Hubot, ed è semplice da configurare e programmare. Non scenderò troppo nei dettagli, ma la documentazione è disponibile online.

slackbot_create_ma_a_friend

Oh, ed è necessario dare un nome al bot. Il nostro è un tipo tosto... così lo abbiamo chiamato Rambot. La logica è programmabile in coffeescript, ed è come scrivere un'app nodejs. Ad esempio è possibile elencare le interfacce di rete:

 

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

Ma torniamo all'argomento principale!

Abbiamo creato uno script campione che riconosce l'utente che invia la richiesta e sceglie randomicamente uno degli altri membri del team:

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}"

Ma non riuscivamo a smettere di programmare, così abbiamo aggiunto una funzione che consente a Rambot di aggiornare gli utenti a cui sono assegnate delle attività tramite l'API JIRA:

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)

E, ovviamente, una funzione basata sull'API Infobip SMS per informare il malcapitato tramite 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!"

Il codice finale ha un aspetto del genere:

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} "

Dal punto di vista dell'utente, le cose stanno così:

rambot_assigned_a_person.png

Non può farci nulla! Se hai un'idea per migliorare ancora Rambot, siamo lieti di ascoltare! L'intero codice è disponibile su GitHub.

Buon DoD!

Di Petar Ducic, Software Engineer / Team Leader