# 🇫🇷 Les 1ers pas de Jay (mon bot devops)

html

Mais qui est Jay?

J'ai une passion inexpliquable pour tout ce qui touche à la fois aux bots, à la CI, au CD etc... Je crois que l'on parle de mouvement ou paradigme ChatOps. J'ai découvert ça tout d'abord en utilisant conjointement Hubot et GitHub. Hubot est un "passe-plats" tellement simple qu'il en est génial, et il a contribué sans nul doute au mouvement ChatOps. Cependant, très vite j'ai eu des besoins pas forcément plus complexes, mais certainement plus diversifiés pour lesquels je devenais dépendant d'intégrations tierces développées pour Hubot, et ces mêmes intégrations n'étaient souvent plus maintenues, ou incomplètes (ou n'existaient pas).

J'avais donc besoin d'un bot que je puisse faire évoluer à mon rythme, de façon simple et qui soit donc versatile au sens anglais du terme: able to adapt or be adapted to many different functions or activities.

C'est comme cela qu'est né Jay (1er du nom, car je compte bien aller plus loin), et disons que ça date officielle de naissance est aujourd'hui, le 22 avril 2019.

Jay est donc un bot multi-canal. Vous pouvez aujourd'hui discuter avec lui à travers:

  • une API REST
  • Slack
  • RocketChat
  • GitLab (par les issues, merge requests, notes)
  • Et bientôt Mattermost, GitHub, MQTT, ...

Et bien sûr, vous pouvez utiliser ces canaux de conversations en même temps.

Avant de rentrer dans le vif du sujet, j'ajoute ceci: j'ai ajouté par le biais de Natural.js des fonctionnalités de Natural Language Processing afin de simplifier l'établissement des règles de discussions avec Jay. Mais nous verrons cela plus loin.

Il existe différentes façon de définir des règles (c'est à dire les facteurs déclenchants des réponses de Jay), vous pouvez bien sûr faire ceci en JavaScript, mais aussi avec des fichiers YAML (une même instance de Jay peut utiliser plusieurs canaux mais aussi plusieurs dictionnaires de règles).

J'écrirais plusieurs articles sur la façon d'utiliser Jay, mais aujourd'hui, nous allons voir comment:

  • Installer Jay
  • Définir des règles
  • Interroger Jays avec des requêtes http via curl
  • Définir des réponses "dynamiques" en bash (😉 il faut lire jusqu'au bout)
  • Interroger Jays dans Slack

Et c'est dejà beaucoup pour un Lundi de Pâques. Prenez votre temps, lisez en plusieurs fois et amusez vous.

  • le site web de Jay sera ici http://www.jay.chat/
  • son repository est ici https://gitlab.com/jay-bot/jay

# Installation rapide

  • git clone git@gitlab.com:jay-bot/jay.git
  • cd jay; npm install
  • si vous installez Jay en local, modifiez votre fichier hosts:

Personnellement j'utilise le service de tunnelling https://burrow.io/ (opens new window), ou https://ngrok.com/ (opens new window)

# Avant de démarrer Jay, il faut lui donner quelques règles

Pour pouvoir discuter avec Jay, il faut pouvoir lui donner quelques règles. Dans un premier temps nous allons discuter avec lui avec de simple requêtes http (avec l'outil curl)

# Création des règles et du fichier d'évènements

👋⚠️ Il y a plusieurs façons de définir des règles pour Jay, la plus simple est d'utiliser un fichier YAML, dans un autre article, nous verrons les autres méthodes.

  • Dans le répertoires /rules créez un répertoire quick-start
  • Dans ce sous-répertoire, créez un fichier my-rules.yml

# Quelques règles

Editez le fichier my-rules.yml:

name: "my-rules"
version: 0.0.0
rules:  
  ping:
    sentence: "@${data.BOT_NAME} send a ping please"
    answer: "🏓 pong ${data.USER_NAME}"
  pong:
    sentence: "@${data.BOT_NAME} send a pong now"
    answer: "ping ${data.USER_NAME} 🏓"
  hello:
    sentence: "@${data.BOT_NAME} hello, how are you?"
    answer: "👋 hello ${data.USER_NAME}, I'm fine, thank you 😃"

# Expliquons à Jay comment il va utiliser les règles pour nous répondre

Pour commencer, nous avons décidé que nous "discuterions" avec Jay avec des requêtes curl. Il faut donc créer au même endroit que notre fichier de règles un fichier JavaScript when.rest.event.js (ℹ️ ce nom est une convention):

Voic comment construire ce fichier:

  1. Tout d'abord nous devons charger la librairie qui nous permet d'utiliser un fichier de règles en yaml:
const DictionnaryFromYAML = require('../../core/models/DictionnaryFromYAML').DictionnaryFromYAML
  1. Puis, nous expliquons à Jay où est ce fichier:
const yamlDictionnary = DictionnaryFromYAML.of(`${__dirname}/my-rules.yml`)
  1. Maintenant nous allons gérer les événements
module.exports =  ({bot, event}) =>  {
  1. Il est possible de passer des options à Jay et ces options seront ré-utilisables dans les règles du fichier yaml en les préfixant par data. (par exemple: ${data.USER_NAME} ou ${data.HELLO})
  let options = {
    BOT_NAME: bot.userName(), 
    USER_NAME: event.userName(), 
    HELLO: "👋 hello 🌍 world",
    CURRENT_PATH: __dirname
  }
  1. Ensuite, passons Jay en "mode écoute" avec la méthode hearing() à laquelle on passe le contenu texte de l'événement:
  bot.hearing({content: event.text()}).when({

  1. Nous allons donc pouvoir déterminer si le message est une Notification pour Jay ou non (NotNotification)
    Notification: text => {
      let disparities = []
      let answers = []

  1. Une fois le texte du message récupéré, nous pouvons le comparer aux règles du dictionnaire yaml avec la méthode search() qui va retourner une méthode de comparaison jaroWinkler(). Cette méthode est issue du domaine du Natural Language Processing, elle permet de calculer une distance entre votre question et les règles du dictionnaire (plus cette distance se rapproche de 1, plus votre question "ressemble" à une règle). Pour cette méthode, Jay utilise un similarityTrigger=0.8(que vous pouvez changer: .jaroWinkler(0.87)), ce qui vous donne une sorte de marge d'erruer en posant vos questions. Jay possède d'autres méthodes de comparaison, nous y reviendront plus tard.
      // Jaro-Winkler
      // best similarity -> 1
      // worst similarity -> 0
      text.search({dictionnary: yamlDictionnary, bot: bot, event: event, options}).jaroWinkler().when({
  1. Et cette méthode vous retournera soit des Disparities, soit des Similarities (une question peut correspondre/ressembler à plusieurs règles)
        Disparities: disparities => {
          // foo
        },
        Similarities: similarities => {
  1. Ici j'ai choisi de n'utiliser que la réponse du premier résultat des similarities et de l'ajouter à mon tableau de réponses (answers):
          answers.push(similarities.first().answer) 
        }
      })

  1. et je renvoie ma réponse:
      // answering ...
      answers.length>0 
        ? bot.httpResponse({content: answers.join("\n")})    
        : bot.httpResponse({content: `😢 sorry @${event.userName()}, can you repeat please?`})  
    },
    NotNotification: text => { 
      // there is a message, but it's not for the bot
      bot.httpResponse({ error: text })  
    }
  })
}

# Le fichier final ressemble à ceci:

const DictionnaryFromYAML = require('../../core/models/DictionnaryFromYAML').DictionnaryFromYAML

const yamlDictionnary = DictionnaryFromYAML.of(`${__dirname}/my-rules.yml`)

module.exports =  ({bot, event}) =>  {

  let options = {
    BOT_NAME: bot.userName(), 
    USER_NAME: event.userName(), 
    HELLO: "👋 hello 🌍 world",
    CURRENT_PATH: __dirname
  }

  bot.hearing({content: event.text()}).when({

    Notification: text => {
      let disparities = []
      let answers = []

      text.search({dictionnary: yamlDictionnary, bot: bot, event: event, options}).jaroWinkler().when({
        Disparities: disparities => {},
        Similarities: similarities => {
          answers.push(similarities.first().answer) 
        }
      })

      answers.length>0 
        ? bot.httpResponse({content: answers.join("\n")})    
        : bot.httpResponse({content: `😢 sorry @${event.userName()}, can you repeat please?`})  
    },
    NotNotification: text => { 
      // there is a message, but it's not for the bot
      bot.httpResponse({ error: text })  
    }
  })
}

Nous pouvons enfin démarrer Jay.

# Démarrer Jay

Pour cela, nous devons lui passer quelques informations comme le port http PORT (par défaut il est égal à 9090), le chemin vers vos règles (RULES_PATH) qui est toujours un sous-dossier de /rules, le nom de votre bot (BOT_NAME), vous pouvez initialiser aussi d'autres variables d'environnement comme MESSAGE que vous pourrez utiliser dans vos règles de cette manière: ${data.MESSAGE}

cd jops
PORT=8080 \
RULES_PATH="quick-start" \
MESSAGE="Hello World, my name is Jay" \
BOT_NAME=jay \
npm start

# Discutons avec Jay

Pour pouvoir utiliser l'intégration REST de Jay, il faut lui envoyer une information au format JSON avec une requête de type POST.

L'information au format JSON doit comporter les éléménts suivants:

{
  "text": "@jay votre question",
  "userName": "vous"
}

👋⚠️ vous devez commencer votre question par @jay (ou le nom de votre bot) pour que Jay comprenne que l'on s'adresse à lui.

C'est parti, faites quelques essais:

  • vous êtes bob et dites "@jay do you want to play ping pong with me?" (vous recevrez un {"content":"🏓 pong bob"})
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay do you want to play ping pong with me?","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock
  • vous pouvez essayer avec une variante de la question, si elle n'est pas trop éloignée de la règle, cela fonctionnera aussi:
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay do you want to play ping-pong, please","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock
  • essayey une autre question: "@jay hello how are you?"
curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay hello how are you?","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

# Allons plus loin avec les règles

# Réponses dynamiques en bash

Lorsque vous récupérez les résultats relatifs à votre question, le tableau similarities, chaque élément de ce tableau possède une réponse (answer), mais il peut aussi posséder le résultat d'une exécution de script (scriptResult) - C'est à dire que vous pouvez exécuter des actions (des scripts shell) si votre question correspond à une règle.

Modifiez le fichier when.rest.event.js de la façon suivante:

Similarities: similarities => {

  if(similarities.first().scriptResult) {
    answers.push(similarities.first().scriptResult)
  } else {
    answers.push(similarities.first().answer) 
  }

}

Et maintenant, allons modifier notre fichier de règles en ajoutant celle-ci:

fourty-two:
  sentence: "@${data.BOT_NAME} what is the meaning of life?"
  script: |
    compute() {
      return_value=42
      echo "$return_value"
    }
    echo "yo ${data.USER_NAME}, 🤔 probably $(compute)"

Et redémarrons Jay et testons cette nouvelle règle

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay what is the meaning of life?","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

Nous devrions obtenir: {"content":"yo bob, 🤔 probably 42\n"}

# Autre exemple: renvoyer le contenu d'un fichier

Ajoutons une nouvelle règle:

emojis:
  sentence: "@${data.BOT_NAME} git emoji"
  script: |
    # the CURRENT_PATH is set in `when.rest.event.js`
    doc=`cat ${data.CURRENT_PATH}/doc.md`
    echo "${doc}"

ℹ️ cette règle nous permet d'aller lire dans un fichier et de renvoyer le contenu de ce fichier comme réponse

Créez le fichier doc.md au même emplacement que le fichier de règles (remarquez que nous utilisons l'option CURRENT_PATH):

_An emoji guide for your commit messages_
> - Improving structure / format of the code: :art: 🎨
> - Fixing a bug: :bug: 🐛
> - Deploying stuff: :rocket: 🚀

Redémarrons Jay et testons cette nouvelle règle:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay git emoji","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

Vous obtiendrez ceci:

{"content":"_An emoji guide for your commit messages_\n> - Improving structure / format of the code: 🎨\n> - Fixing a bug: 🐛\n> - Deploying stuff: 🚀\n"}

# Autre exemple: utiliser node.js

Cela fonctionnera avec d'autres outils bien sûr, mais sachant que vous utilisez node.js pour exécuter Jay, vous avez forcément node.js sur la machine où est installé Jay.

Une fois de plus, ajoutons une nouvelle règle:

give_me_a_number:
  sentence: "@${data.BOT_NAME} give me a number please"
  script: |
    node ${data.CURRENT_PATH}/number.js

Créez le fichier number.js au même emplacement que le fichier de règles:

let num = Math.random() * (100 - 1) + 1
console.log(num)

Redémarrons Jay et testons cette nouvelle règle:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay give me a number please","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

Et nous obtiendrons quelque chose comme ceci:

{"content":"68.50143643414093\n"}

# Et si je voulais passer un paramètre dans ma question?

En fait, pour cela, le plus simple est d'ajouter (dans when.rest.event.js) par exemple, une variable QUESTION dans options qui prendra le contenu de votre question, que vous pourrez retraiter plus tard:

  let options = {
    BOT_NAME: bot.userName(), 
    USER_NAME: event.userName(), 
    HELLO: "👋 hello 🌍 world",
    CURRENT_PATH: __dirname,
    QUESTION: event.text()
  }

Puis d'écrire une règle comme celle-ci:

repeat:
  sentence: "@${data.BOT_NAME} repeat after me"
  script: |
    # à vous d'extraire les éléments nécessaires:
    echo "${data.QUESTION}" | grep -Eo '[0-9]+$'

Et de la tester comme ceci:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay repeat after me: 42","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

Et vous obtiendrez une réponse comme celle-là:

{"content":"42\n"}

ℹ️ je travail sur une méthode plus user friendly pour faire ça, mais pour le moment cela fait le job.

# Je voudrais pouvoir utiliser plusieurs dictionnaires

Vous pouvez mixer plusieurs dictionnaires pour un même bot (et avec des formats différents, mais ce sera pour un autre article).

Donc, toujours dans votre répertoire /rules/quick-start, créez un nouveau fichier de règle other-rules.yml avec le contenu suivant:

name: "other-rules"
version: 0.0.0
initialize: | # run it before each script
  # intialize some variables and functions
  TITLE="I 😍 Jay"
  hello() {
    echo "👋 my friend $1 😃"
  }
  # variables and functions are intialized
rules:  
  hello:
    sentence: "@${data.BOT_NAME} say hello"
    script: |
      echo "Title: ${TITLE}"
      echo "Message: ${data.HELLO}"
      hello "${data.USER_NAME}"

👋ℹ️ vous remarquerez la nouvelle clé initialize qui vous permet de définir des fonctions et des variables d'environnement utilisables dans vos scripts de réponse.

Vous pouvez redémarrer Jay et tester cette nouvelle règle:

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"text":"@jay say hello","userName":"bob"}' \
  http://jay.test:8080/rest/knock-knock

Et vous obtiendrez ceci:

{"content":"Title: I 😍 Jay\nMessage: 👋 hello 🌍 world\n👋 my friend bob 😃\n"}

# Je voudrais pouvoir utiliser Jay en même temps dans Slack et en mode REST

Il est possible d'utiliser ces dictionnaires de règles pour discuter avec Jay à partir de Slack (mais aussi RocketChat et/ou GitLab et bientôt d'autres outils).

Pour cela c'est très simple (même si il y a un peu de paramétrage à effectuer). Dans un 1er temps, toujours dans le même répertoire /rules/quick-start, créez un nouveau fichier JavaScript when.slack.event.js (ℹ️ cette fois-ci aussi ce nom est une convention, donc ne le changez pas):

const DictionnaryFromYAML = require('../../core/models/DictionnaryFromYAML').DictionnaryFromYAML

const yamlDictionnary = DictionnaryFromYAML.of(`${__dirname}/my-rules.yml`)
const otherYamlDictionnary = DictionnaryFromYAML.of(`${__dirname}/other-rules.yml`)

module.exports =  ({bot, event}) =>  {

  // ⚠️ it's important
  let channel = event.channelName()

  let options = {
    BOT_NAME: bot.userName(), 
    USER_NAME: event.userName(), 
    HELLO: "👋 hello 🌍 world",
    CURRENT_PATH: __dirname,
    QUESTION: event.text()
  }

  bot.hearing({content: event.text()}).when({

    Notification: text => {
      let disparities = []
      let answers = []

      // Jaro-Winkler
      // best similarity -> 1
      // worst similarity -> 0
      text.search({dictionnary: yamlDictionnary, bot: bot, event: event, options}).jaroWinkler().when({
        Disparities: disparities => {},
        Similarities: similarities => {

          if(similarities.first().scriptResult) {
            answers.push(similarities.first().scriptResult)
          } else {
            answers.push(similarities.first().answer) 
          }
        }
      })

      // Levenshtein
      // best similarity -> 0
      // worst similarity -> N
      text.search({dictionnary: otherYamlDictionnary, bot: bot, event: event, options}).levenshtein().when({
        Disparities: disparities => {},
        Similarities: similarities => {

          if(similarities.first().scriptResult) {
            answers.push(similarities.first().scriptResult)
          } else {
            answers.push(similarities.first().answer) 
          }
        }
      })

      // answering ...
      answers.length>0 
        ? bot.sendToSlack(channel, answers.join("\n"))
        : bot.sendToSlack(channel, `😢 sorry @${event.userName()}, can you repeat please?`)

    },
    NotNotification: text => { 
      // there is a message, but it's not for the bot
      console.log(text)
    }
  })

}

Nous avons donc utilisé les mêmes dictionnaires, ⚠️ mais attention aux 2 choses importantes qui changent dans ce nouveau fichier:

  1. Vous aurez besoin du nom du "channel" sur lequel a lieu la discussion:
let channel = event.channelName()
  1. Et cette fois ci, vous allez répondre d'une manière différente avec une méthode spécifique du bot sendToSlack():
answers.length>0 
  ? bot.sendToSlack(channel, answers.join("\n"))
  : bot.sendToSlack(channel, `😢 sorry @${event.userName()}, can you repeat please?`)

# Paramétrage de Slack

Avant de relancer Jay nous devons paramétrer Slack.

👋⚠️ Si vous travaillez en local, vous aurez besoin d'un outil de "tunneling" http pour que Slack puisse vous renvoyer les réponses. J'utilise https://burrow.io/ (opens new window) et dans mon cas pour cet exemple, l'url pour communiquer avec mon bot sera http://jay.burrow.io (opens new window).

  • Dans Slack, aller dans Administration > Manage Apps > Custom Integration

# Incoming Webhook

Vous allez créer un Incoming Webhook qui va permettre au bot de "poster" dans Slack:

  • Allez dans Incoming Webhook
  • Clickez sur Add Configuration
  • Sélectionnez un/une Channel (par exemple random, ensuite vous pourrez simplement changer de channel dans votre code)
  • Clickez sur Add Incoming Webhook
  • Copiez l'url du webhook pour l'utiliser dans les variables d'environnement suivantes:

Si votre url ressemble à ceci https://hooks.slack.com/services/YYYY/XXXXXX/ZZZ, définissez vos variables d'environnement de la manière suivante:

SLACK_HOOKS_BASE_URL="https://hooks.slack.com/services/" \
SLACK_INCOMING_TOKEN="YYYY/XXXXXX/ZZZ" \

# Outgoing Webhook

Vous allez créer des Outgoing Webhook qui vous vont permettre de "parler" au bot (vous avez autant de Outgoing Webhooks que de channels utilisé(e)s):

  • Allez dans Outgoing Webhook
  • Clickez su Add Configuration
  • Clickez su Add Outgoing Webhook
  • Sélectionnez un(e) Channel ⚠️ si vous souhaitez utilisez plusieurs channels, vous devez créer autant de Outgoing Webhooks
  • Ajouter un Trigger Word (par exemple le nom de votre bot @jay)
  • Ajouter une Callback URL (dans mon cas: http://jay.burrow.io/slack/knock-knock) cette url se termine toujours par slack/knock-knock
  • Renseignez les variable comme ceci (avec les tokens des channels dans le même ordre que les channels):
SLACK_CHANNELS="general,random" \
SLACK_OUTGOING_TOKENS="AAAAA,BBBBB" \

# Lancement de Jay

  • Tout d'abord lancez votre système de "tunneling http"
  • Ensuite, vous pouvez lancer Jay de la manièrer suivante:
PORT=8080 \
RULES_PATH="quick-start" \
MESSAGE="Hello World, my name is Jay" \
BOT_NAME=jay \
SLACK_HOOKS_BASE_URL="https://hooks.slack.com/services/" \
SLACK_INCOMING_TOKEN="YYYY/XXXXXX/ZZZ" \
SLACK_CHANNELS="general,random" \
SLACK_OUTGOING_TOKENS="AAAAA,BBBBB" \
npm start

Mainteant, comme vous avez les 2 intégrations présentes dans /rules/quick-start (when.rest.event.js et when.slack.event.js) vous pouvez dialoguer avec Jay de 2 manières différentes.

Il ne vous reste plus qu'à essayer vos dictionnaires de règles dans Slack cette fois-ci. s C'est tout pour aujourd'hui. La prochaine fois nous verrons comment définir des règles pour Jay en pur JavaScript. Mais aussi comment discuter avec Jay directement à partir de commentaires dans GitLab, et bien d'autres choses 😉.

Last Articles