Chez Nuxeo, nous cherchons toujours de nouvelles occasions d'ajouter de nouvelles fonctionnalités à Nuxeo Platform. En combinant cette volonté d'innover et ma passion pour les produits Amazon, j'ai créé il y a quelques mois un skill Nuxeo pour Alexa qui nous permet de parcourir et chercher les documents stockés dans Nuxeo Platform. Lors de la sortie du nouvel Echo Show, j'ai tout de suite vu une possibilité d'améliorer l'expérience Nuxeo - Alexa en affichant les documents sur l'écran de l'Echo Show. Cela signifie que vous pouvez demander à Alexa de parcourir vos ressources numériques et de vous montrer les résultats sur l'écran sans avoir à bouger le petit doigt !

Voyons ensemble comment configurer ce skill et le résultat.

Récap du skill précédent

Dans mon précédent article sur Alexa, je vous ai montré comment développer un skill avec un écran d'authentification permettant de lier votre compte Alexa à votre instance Nuxeo.

Il comptait 3 commandes principales

  • Search, pour parcourir vos documents
  • GetLast, pour consulter vos derniers documents
  • Tasks, pour voir vos dernières tâches

L'architecture ressemblait à ça :

Architecture

Voyons maintenant les étapes à suivre pour afficher les résultats de la recherche sur l'Echo Show.

Gestion de l'affichage

Pour afficher vos ressources sur l'Echo Show, il vous suffit d'inclure une directive supplémentaire dans votre réponse.

  {
    "type": "Display.RenderTemplate",
    "template": {...}
  }

6 types d'organisation sont disponibles dans la liste actuelle :

BodyTemplate1 BodyTemplate2 BodyTemplate3
BodyTemplate1 BodyTemplate2 BodyTemplate3
BodyTemplate6 ListTemplate1 ListTemplate2
BodyTemplate6 ListTemplate1 ListTemplate2

Tasks

Pour les tâches, j'ai décidé d'utiliser ListTemplate1 car les miniatures ne sont pas indispensables. J'ai mis à jour le code et ajouté quelques lignes. Voici le code avant et après :

Avant

var tasks = '';
for (let id in doc.entries) {
    let res = "Task " + i++ + " " + ...;
    tasks += res;
    response.say(res);
}

Après

var tasks = '';
var listItems = [];
for (let id in doc.entries) {
    let res = "Task " + i++ + " " + ...;
    tasks += res;
    response.say(res);
    listItems.push(
      {
        ...
        "textContent": {
            "primartyText": {
              ...
            }
        }
      });
}
let template = {
  "type": "ListTemplate1",
  "token": "workflow",
  "title": "Your workflow tasks",
  "listItems": listItems
};
response.directive({"type": "Display.RenderTemplate", "template": template });

Pour afficher nos documents à partir d'une requête Search ou GetLast, j'ai choisi ListTemplate2 car je voulais aussi afficher leurs miniatures.

Mais comme l'URL de la miniature nécessite une authentification, sa récupération entraîne une erreur. Il faut ruser et faire passer la requête par un proxy. Au lieu d'utiliser l'URL de l'API REST de la miniature :

{nuxeo_url}/api/v1/{uuid}/@rendition/thumbnail

Nous allons utiliser un proxy passant par notre serveur d'authentification :

https://nuxeo-auth.loopingz.com/thumbnail.php?token={token}&uid={uid}

Proxy pour les miniatures

La nouvelle architecture ressemble à ça :

New Architecture

Le proxy utilisé pour les miniatures est assez simple. Il utilise le token d'authentification pour initier le client d'API Nuxeo en PHP puis effectue un GET sur la miniature. En cas d'échec, il retournera une image par défaut pour le document.

...
try {
   $client = new NuxeoClient($url);
   $client = $client->setAuthenticationMethod(new TokenAuthentication($token));

   $doc = $client->automation('Document.Fetch')->param('value', $uid)->execute(Document::className);
   $thurl = $url."/api/v1/id/".$uid."/@rendition/thumbnail";
   $res = $client->get($thurl);
   header("Content-Type: ".$res->getContentType());
   header("Content-Length: " . $res->getContentLength());
   print($res->getBody());
} catch (Exception $e) {
  defaultImage();
}

Sécurité

Comme le token passe maintenant par plusieurs URL et n'était encodé qu'en base64, j'ai décidé de le sécuriser un peu plus et d'utiliser un chiffrement AES-256 avec un secret partagé entre le serveur d'authentification et la fonction Lambda.

J'ai donc ajouté le chiffrement au linker et le déchiffrement au proxy utilisé pour les miniatures.

require 'secret.php';

function encrypt($data) {
  global $SECRET;
  $pass = openssl_digest($SECRET,"sha256", true);
  $iv = openssl_random_pseudo_bytes(16);
  $algo = "aes-256-ctr";
  $options = OPENSSL_RAW_DATA;
  $encData = openssl_encrypt($data, $algo, $pass, $options, $iv);
  return base64_encode($iv.$encData);
}

function decrypt($data) {
  global $SECRET;
  $pass = openssl_digest($SECRET,"sha256", true);
  $algo = "aes-256-ctr";
  $options = OPENSSL_RAW_DATA;
  $plain = base64_decode($data);
  return openssl_decrypt(substr($plain,16), $algo, $pass, $options, substr($plain,0,16));
}

L'astuce était ensuite d'aligner le chiffrement de PHP et de NodeJS pour le skill Alexa.

const secret = require('./secret');

...

let tokenRaw = request.getSession().details.accessToken;
let raw = Buffer.from(tokenRaw, 'base64');
let iv = raw.slice(0,16);
let enc = raw.slice(16);
let cipher = crypto.createCipheriv(algo, secret, iv);
let dec = cipher.update(enc, 'ascii', 'ascii');
dec += cipher.final('ascii');

Le chiffrement est assez standard avec une clé partagée de 256 bits et la génération d'un vecteur d'initialisation IV aléatoire. Les deux sont utilisés pour chiffrer nos données puis pour concaténer les vecteurs IV avec les données chiffrées.

Le système en action

Voici une démo du résultat :

Vous trouverez les sources dans le repository GitHub. N'hésitez pas à essayer et à nous faire part de vos commentaires !