Créer une skill Amazon Alexa avec un "Endpoint" en PHP

23 juillet 2018 - : Amazon Alexa - : 2 commentaire.s

Créer une skill Amazon Alexa avec un "Endpoint" en PHP

Lorsque je me suis intéressé au développement de skills pour Amazon Alexa, j'ai cherché à savoir comment gérer la partie serveur.

Il faut se rendre à l'évidence, énormément de ressources existent en Anglais, très peu en Français.

De nombreux tutos existent également mais en particulier pour NodeJS et Python, mais pas vraiment pour PHP.

Dans ce tuto, nous allons aborder les 2 parties distinctes d'une skill Amazon Alexa, à savoir la partie "Amazon" et la partie distante, appelée Endpoint.

Quelques bases

Vous devez dans un premier temps lister les différentes intéractions que l'utilisateur pourra avoir avec Amazon Alexa. Dans cet article, nous allons voir comment générer un service qui permettra de demander un contenu aléatoire stocké dans une base de données MySQL.

L'utilisateur pourra donc dire, par exemple :

"Ouvre mon appli et choisis un contenu"

"Ouvre mon appli pour donner un contenu"

"Ouvre mon appli et donne un contenu"

Dans toutes ces phrases, "mon appli" sera l'invocation, qui permettra à Amazon Alexa de savoir quelle "skill" doit être ouverte.

"choisis un contenu", "donner un contenu" et "donne un contenu" seront des intentions, commandes permettant de déclencher une action.

Création d'une skill

Tout d'abord, vous devez créer un compte développeur sur le site Amazon développeur.

Lorsque votre compte sera créé, vous aurez accès à toutes les ressources nécessaires. Vous devriez voir cette page après votre connexion.

Accueil du compte développeur Amazon

Une fois sur cette page, vous pouvez accéder aux ressources permettant de développer votre Skill Alexa.

Cliquez sur Amazon Alexa, puis sur Ajouter des skills à Alexa et enfin sur Démarrez avec Alexa.

Vous arriverez sur l'écran suivant :

Accueil Alexa Skills

Depuis cet écran, cliquez sur Start a Skill.

Créer Alexa Skills

Après avoir cliqué sur Create Skill, vous aurez à remplir les informations de base de la Skill, son nom, sa langue et son type. Nous allons prendre Custom puis Cliquer sur Create Skill de nouveau.

Créer Alexa Skills

Vous arrivez maintenant sur la console de la Skill nouvellement créée.

Créer Alexa Skills

Configurer la Skill

L'invocation

Dans cette console, nous allons définir les différentes intéractions avec la Skill, à commencer par l'invocation.

L'invocation est la commande vocale qui servira à démarrer la skill. Ici, pour l'exemple nous allons mettre "démo nouvelle techno".

Cliquez sur Invocation sur la gauche et remplissez comme ci-après.

Invocation Alexa Skills

Puis cliquez sur Save Model (en haut)

Les intentions (Intents)

Après avoir créé l'invocation, nous allons créer les intentions (Intents).

Ce sont les "commandes" que les utilisateurs pourront prononcer pour accéder à certaines fonctions de la Skill.

Cliquez sur le +Add situé à côté de Intents (côté gauche)

Invocation Alexa Skills

Vous pouvez choisir des intentions prédéfinies ou créer une intention personnalisée. Nous allons prendre cette option et utiliser le champ Create custom intent.

Dans ce champ, entrez le nom donné à l'intention, par exemple "contenu" puis cliquez sur le bouton Create custom intent

Viennent enfin, dans les intentions, les énoncés (Utterances).

Il s'agit des phrases que l'utilisateur devra prononcer pour activer cette intention.

Plus haut dans cet article, nous avions défini 3 phrases à prononcer, entrez les comme sur l'exemple ci-dessous sans oublier de cliquer sur le + en fin de ligne à chaque phrase.

Utterances Alexa Skills

Vous noterez que nous avons retiré les mots de liaison (de, pour, et...) qui sont automatiquement compris par Alexa.

Nous verrons dans un autre article que des variables (appelés slots) peuvent être inclues dans les énoncés.

Cliquez maintenant sur Save Model.

Le point d'arrivée (Endpoint)

Notre Skill doit se connecter à un serveur pour pouvoir effectuer les actions que nous souhaitons lui attribuer.

Cliquez sur Endpoint (à gauche) pour configurer cette partie.

Dans cette page, 2 options s'offrent à vous. Une fonction Lambda sur un serveur Amazon, développée en NodeJS ou Python, ou un serveur https du langage de votre choix.

ATTENTION : le serveur doit être muni d'un certificat SSL

Nous allons développer la 2ème option. Sélectionnez l'option HTTPS puis entrez l'URL vers votre fichier php et choisissez l'option qui vous correspond pour le certificat. Puis cliquez sur Save Endpoints.

Endpoint Alexa Skills

Enfin, nous allons compiler notre Skill en cliquant sur Build (en haut) puis sur Build Model (à droite).

La partie Skill est terminée. Nous allons maintenant voir le Web Service.

Le Web Service en PHP

Nous voici entrant dans le vif du sujet, le PHP.

Vérifications obligatoires

ATTENTION, plusieurs contraintes sont à prendre en compte pour développer le web service.

  • Le service doit être accessible depuis internet
  • Le service doit supporter SSL/TLS
  • Le service doit accepter les requêtes sur le port 443.
  • Le service doit valider que les requêtes viennent d'Alexa

Ceci étant dit, voici les différentes étapes en PHP.

ATTENTION (bis) : les exemples de code ci-dessous doivent être adaptés en fonction de votre contexte.

Nous commençons par récupérer le contenu de la requête envoyée par Alexa, en JSON et la traduisons en un tableau PHP.

$json = file_get_contents('php://input');
$requete = json_decode($json);

Nous avons donc dans la variable $requete le contenu envoyé par Alexa.

Nous devons maintenant vérifier si la requête vient bien d'Amazon. Nous vérifions donc si l'adresse IP expéditrice correspond à celles d'Amazon.

$validIP = array("72.21.217.","54.240.197.");
$isAllowedHost = false;

// Vérifions si Amazon est à l'origine de la requête
foreach($validIP as $ip){
	if (stristr($_SERVER['REMOTE_ADDR'], $ip)){
		$isAllowedHost = true;
		break;
	}
}

// Si Amazon n'est pas expéditeur
if ($isAllowedHost == false) ThrowRequestError(400, "Forbidden, your Host is not allowed to make this request!");

unset($isAllowedHost);

Nous vérifions ensuite que cette requête vient bien de notre application. Nous devons pour celà vérifier qu'elle contient notre ApplicationID. Celui-ci peut être récupéré depuis l'écran Your Skills.

if (strtolower($requete->session->application->applicationId) != strtolower("amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") || empty($EchoReqObj->session->application->applicationId))
{
	ThrowRequestError(400, "Forbidden, unkown Application ID!");
}

Vient ensuite la vérification de la chaine de signature SSL. En effet, Amazon joint à la requête une url du type "https://s3.amazonaws.com/echo.api/../echo.api/echo-api-cert.pem", cette url doit être simplifiée afin d'arriver à "https://s3.amazonaws.com/echo.api/echo-api-cert.pem" (détails ici)

// Vérification de la chaine de signature SSL par expression régulière
if (preg_match("/https:\/\/s3.amazonaws.com(\:443)?\/echo.api\/*/i", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) == false){
	ThrowRequestError(400, "Forbidden, unkown SSL Chain Origin!");
}
// Vérification du certificatPEM
// Récupération en cache
$local_pem_hash_file = sys_get_temp_dir() . '/' . hash("sha256", $_SERVER['HTTP_SIGNATURECERTCHAINURL']) . ".pem";
if (!file_exists($local_pem_hash_file)){
	file_put_contents($local_pem_hash_file, file_get_contents($_SERVER['HTTP_SIGNATURECERTCHAINURL']));
}
$local_pem = file_get_contents($local_pem_hash_file);
if (openssl_verify($json, base64_decode($_SERVER['HTTP_SIGNATURE']) , $local_pem) !== 1){
	ThrowRequestError(400, "Forbidden, failed to verify SSL Signature!");
}
// Vérifications complémentaires
$cert = openssl_x509_parse($local_pem);
if (empty($cert)) ThrowRequestError(400, "Certificate parsing failed!");
// SANs Check
if (stristr($cert['extensions']['subjectAltName'], 'echo-api.amazon.com') != true) ThrowRequestError(400, "Forbidden! Certificate SANs Check failed!");

// Vérification de l'expiration
if ($cert['validTo_time_t'] < time()){
	ThrowRequestError(400, "Forbidden! Certificate no longer Valid!");
	// Deleting locally cached file to fetch a new at next req
	if (file_exists($local_pem_hash_file)) unlink($local_pem_hash_file);
}
// Nettoyage
unset($local_pem_hash_file, $cert, $local_pem);

Enfin, nous vérifions que la requête a été envoyée depuis moins d'une minute.

if (time() - strtotime($requete->request->timestamp) > 60) ThrowRequestError(400, "Request Timeout! Request timestamp is to old.");

Ces vérifications terminées, nous allons pouvoir lire la requête et répondre à Alexa.

Traitement de la requête

Maintenant que les vérifications sont terminées, nous allons traiter le contenu.

Pour commencer, nous allons vérifier le type de requête.

Il peut s'agir de "LaunchRequest" pour le démarrage de la Skill, d'une "IntentRequest" pour une intention ou d'une "SessionEndedRequest" si l'utilisateur souhaite arrêter.

Pour savoir quelle requête nous concerne, nous allons utiliser les conditions suivantes :

// On récupère le type de requête
$RequestType = $requete->request->type;

if ($RequestType == "LaunchRequest"){
	// Ici le code pour LaunchRequest
}
elseif ($RequestType == "SessionEndedRequest"){
	// Ici le code pour SessionEndedRequest
}
elseif ($RequestType == "IntentRequest"){
	// Ici le code pour IntentRequest
}else{
	ThrowRequestError();
}

Exemple de traitement de "LaunchRequest"

if ($RequestType == "LaunchRequest"){
	$return_defaults = array(
		'version' => '1.0',
		'sessionAttributes' => array(
			'countActionList' => array(
				'read' => true,
				'category' => true
			)
		) ,
		'response' => array(
			'outputSpeech' => array(
				'type' => "PlainText",
				'text' => "Bienvenue dans Démo Nouvelle Techno, dites \"ouvre Démo Nouvelle Techno et choisis un contenu\""
			) ,
		) ,
                // Cette ligne gardera la session active
		'shouldEndSession' => false
	);
	$ReturnValue = json_encode($return_defaults);
}

Exemple de traitement d'une "IntentRequest"

if ($RequestType == "IntentRequest"){
	if ($requete->request->intent->name == "contenu"){ // Nom de l'Intent Alexa
		include 'mysqlPDOConnect.php';
		$stmt = $dbh->prepare('SELECT * FROM conteny WHERE id IN (SELECT id FROM (SELECT id FROM contenu ORDER BY RAND() LIMIT 1) t)'); // Sélection d'un contenu aléatoire
		$stmt->execute();
		$result = $stmt->fetch();
		$SpeakPhrase = $result['contenu'];
		$ReturnValue = json_encode(array(
			'version' => "0.1",
			'sessionAttributes' => array(
				'countActionList' => array(
					'read' => true,
					'category' => true
				)
			) ,
			'response' => array(
				'outputSpeech' => array(
					'type' => "PlainText",
					'text' => $SpeakPhrase
				) ,
			) ,
			'shouldEndSession' => true
		));
	}
}

Bien sûr, il faut retourner tout ceci à Alexa, notre fichier se terminera donc par

header('Content-Type: application/json');
header("Content-length: " . strlen($ReturnValue));
echo $ReturnValue;

Sans oublier la gestion des erreurs

function ThrowRequestError($code = 400, $msg = 'Bad Request'){
      GLOBAL $SETUP;
      http_response_code(400);
      echo "Error " . $code . "\n" . $msg;
      exit();
}

Attention, quelle que soit l'erreur, Amazon demande un code 400.

Commentaires

Nrl

Bonjour, très didactique cet article. j'ai tout suivi et je pense avoir compris comment cela fonctionne. par contre à chaque fois que j'appelle mon endpoint php depuis l'interface de test, je vois bien l'url dans les services demandés mais il répond systématiquement qu'il ne trouve pas le endpoint. j'ai ajouté du log dans le fichier php et rien n'arrive jusqu'au serveur. une idée ? 

Le 29 juillet 2018
Nouvelle-Techno.fr

Bonjour,

Le endpoint est-il bien en HTTPS avec un certificat approuvé ou X509 ?

Le 29 juillet 2018

Laisser un commentaire