
Partager:
Je suis développeur JavaScript et éducateur de développeurs chez Vonage. Au fil des ans, j'ai été très intéressé par les modèles, Node.js, les applications Web progressives et les stratégies offline-first, mais ce que j'ai toujours aimé, c'est une API utile et bien documentée. Mon objectif est de faire en sorte que votre expérience de l'utilisation de nos API soit la meilleure possible.
Machines d'état pour les bots de messagerie WhatsApp avec Node.js
Temps de lecture : 7 minutes
Dans un serveur web classique, il n'est pas nécessaire de se préoccuper de l'état. Un utilisateur vous envoie une requête et vous lui fournissez une réponse. Il n'est pas nécessaire que l'application se guide elle-même à travers un chemin de choix et d'actions ; c'est l'utilisateur final qui s'en charge. Cependant, un bot fonctionne différemment.
Bien qu'un utilisateur final entame une conversation avec un robot, ce dernier doit définir le chemin à suivre, en posant des questions à l'utilisateur pour l'informer des étapes suivantes potentielles. Lorsque le robot ne se contente pas de répondre à des questions, mais qu'il guide l'utilisateur final à travers une série d'étapes, on parle de machine à états.
L'implémentation d'une machine à états dans un bot de messagerie est un peu délicate parce que les bots de messagerie n'ont pas de concept organique d'état. Par défaut, un message envoyé à un serveur que vous contrôlez arrive sans session, sans état, ni aucune autre information sur l'image globale dont le message individuel pourrait faire partie. En réalité, tout ce que cela signifie, c'est que vous devrez stocker manuellement le dernier état d'une "session" entre votre serveur et un numéro de téléphone donné. En réalité, une application web doit faire la même chose. Les plateformes et les bibliothèques font généralement le travail à notre place.
Conditions préalables
Notre serveur bot utilisera l'un de ces serveurs web traditionnels d'une manière non traditionnelle. Pour suivre cet exemple, vous aurez besoin de :
A base de données une base de données installée et configurée
Un compte Vonage Developer
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
Le code que nous allons examiner fait partie d'un projet plus vaste de bot WhatsApp. projet d'exemple de bot WhatsApp sur Glitch. Vous pouvez également copier et coller ce code, ou le remixer pour commencer avec une application fonctionnelle.
Points d'extrémité du serveur
Toutes les instructions et les demandes des utilisateurs finaux sont acheminées par un seul point d'accès sur notre serveur. C'est à notre serveur de les analyser et de déterminer ce qu'il convient de faire. Les messages entrants seront des requêtes POST contenant le message lui-même et ses métadonnées. Nous pouvons utiliser Express pour les traiter, puis transmettre des types spécifiques à d'autres gestionnaires par la suite.
Tout d'abord, nous allons mettre en place un serveur Express dans server.jsen le configurant pour qu'il analyse le corps des requêtes entrantes et qu'il serve des pages statiques. Nous pouvons également définir nos états. J'ai utilisé des noms de propriétés explicatifs correspondant à des entiers pour éviter d'avoir à faire des comparaisons de chaînes de caractères. Il y en aura beaucoup plus tard !
const fs = require('fs');
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static('public'));
const states = {
waiting: 0,
getUsername: 1,
getEmail: 2,
getAddress: 3,
confirmPayment: 4
};
// CONFIGURE DATABASE
// APP CODE
const listener = app.listen(process.env.PORT, () => {
console.log("Your app is listening on port " + listener.address().port);
});
Comme l'API de Vonage fournit deux webhooks, il y a deux points d'extrémité dans le serveur. Mais dans cet exemple, un seul fera réellement quelque chose. Pour garder les choses en ordre, le point de terminaison /status se contente d'accuser réception de toutes les demandes qu'il reçoit. Le point d'extrémité /inbound est l'endroit où le travail de l'application commence vraiment.
Avant le point de terminaison /inbound nous créons une instance Vonage que nous pouvons utiliser pour envoyer des réponses. L'exemple utilise l'API Sandbox de Vonage Message, ce qui nécessite de définir l'option apiHost.
// APP CODE
// this endpoint receives information about events in the app
app.post('/status', function(req, res) {
res.status(204).end();
});
const Vonage = require('@vonage/server-sdk');
const vonage = new Vonage({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
applicationId: process.env.APP_ID,
privateKey: __dirname + '/.data/private.key'
},{
apiHost: 'https://messages-sandbox.nexmo.com/'
});
app.post('/inbound', function(req, res) {});
app.post('/signup', function(req, res) {});
function setUsername(phone, username) {}Avant de développer le gestionnaire de messages entrants et les autres fonctions, nous allons configurer les autres éléments dont nous avons besoin.
Configuration des Webhooks
Pour envoyer des messages dans les deux sens entre un compte d'application de messagerie personnelle et le serveur, vous devez configurer l'API Messages de Vonage. Les Applications envoyées à un compte appartenant à Vonage ou à un compte que vous avez enregistré avec une application Vonage seront transmises au point de terminaison que vous avez spécifié. Il y a deux façons de procéder, selon que vous utilisez ou non l'Environnement de test de l'API Messages.
Si vous utilisez l'Environnement de test, vous n'aurez pas besoin de créer une application pour tester la messagerie. Vous pouvez configurer vos webhooks à partir de la page de l'Environnement de test. Il vous suffit de fournir un point de terminaison pour gérer les messages entrants et un autre pour gérer les messages d'état sur votre serveur accessible au public.

Si vous possédez un numéro pour la messagerie, vous pouvez configurer les webhooks dans votre application. Lors de la création de l'application, faites défiler vers le bas jusqu'à Capacités et activez l'option "Messages". Cela fait apparaître les champs dans lesquels vous pouvez spécifier les points de terminaison des webhooks.

Mise en place d'un magasin de données
L'état que vous stockez peut être simple ou complexe, selon vos besoins. Outre l'état de l'interaction du serveur avec un utilisateur donné, vous pouvez souhaiter stocker des informations que vous conserveriez dans une variable de session sur un serveur web traditionnel. Cependant, ces informations sont parfois stockées dans la session pour éviter d'avoir à les rechercher dans la base de données, ce qui présente peu d'avantages. Les informations supplémentaires contenues dans votre base de données d'état sont probablement mieux utilisées pour compléter le contexte de l'état actuel.
Nous commencerons par créer la base de données elle-même, puis nous ajouterons une table d'état. Cet exemple ne sera pas complexe. Au lieu de contenir plusieurs colonnes pour les différents éléments d'information dont un état donné pourrait avoir besoin, nous utiliserons simplement une colonne fourre-tout appelée memo:
// CONFIGURE DATABASE
const dbFile = './.data/sqlite.db';
var exists = fs.existsSync(dbFile);
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database(dbFile);
db.serialize(function(){
if (!exists) {
db.run('CREATE TABLE State (phone NUMERIC UNIQUE, state NUMERIC, memo TEXT)')
db.run('CREATE TABLE Users (phone NUMERIC UNIQUE, username TEXT, email TEXT, address TEXT)');
}
});Nous créons également une table Users car le processus avec état dans cet exemple sera l'enregistrement de l'utilisateur.
Contrôle de l'État
Désormais, lorsque nous recevons un message entrant, nous sommes prêts à vérifier si notre robot est au milieu d'un processus avec état avec l'expéditeur. Nous ne regarderons pas le contenu du message avant d'avoir vérifié la base de données des états et déterminé l'état que nous attendons. En supposant qu'il soit possible de quitter un processus avec état, vous pourriez choisir de vérifier si l'utilisateur souhaite le faire avant d'aller dans la base de données. Mais la plupart des processus, une fois lancés, ont besoin d'être nettoyés s'ils sont annulés, de sorte qu'il est tout aussi probable que vous souhaitiez consulter la base de données de toute façon.
La principale chose dont nous avons besoin dans le corps de la requête est le numéro de téléphone de l'expéditeur. Nous pouvons l'utiliser pour interroger la base de données des états et, si nous trouvons que le numéro a un état, appeler une fonction à laquelle il correspond. Si ce n'est pas le cas, nous pouvons transmettre l'ensemble du message à une fonction parseIncoming qui recherchera de nouvelles instructions.
app.post('/inbound', function(req, res) {
let phone = req.body.from.number;
let message = req.body.message.content;
db.get('SELECT * FROM State WHERE (phone = $phone)', {
$phone: phone
}, function(error, userState) {
switch(userState.state) {
case states.getUsername:
setUsername(phone, message.text);
break;
case states.getEmail:
setEmail(phone, message.text, true);
break;
case states.getAddress:
setAddress(phone, message.text, true);
break;
case states.confirmPayment:
completeBuy(phone, message.text);
break;
default:
parseIncoming(phone, message);
}
});
res.status(204).end();
}); Mise à jour de l'État
En supposant que nous poursuivions le processus, l'état suivant sera déterminé par l'état actuel. Au départ, bien sûr, il n'y en aura pas. L'utilisateur doit entrer dans un processus avec état d'une manière ou d'une autre. Pour la plupart des états disponibles dans l'exemple, ce moyen consiste à s'inscrire.
Le modèle du point de terminaison /signup correspond à la moitié de celui que suivront la plupart des autres étapes du processus d'inscription. Il envoie un message au numéro de téléphone figurant dans le corps de la requête (dans ce cas, il s'agit d'un formulaire web et non d'un message), invitant l'utilisateur à passer à l'étape suivante. Il crée ensuite une nouvelle ligne dans la base de données des états pour marquer la place de l'utilisateur dans le processus. Dans les étapes suivantes, il s'agira d'une mise à jour :
app.post('/signup', function(req, res) {
let phone = req.body.number;
vonage.channel.send(
{ type: 'whatsapp', number: phone },
{ type: 'whatsapp', number: process.env.WHATSAPP_NUM },
{ content: {
type: 'text',
text: 'Welcome to Nice Cool Shoes! What should we call you?'
}}, (e, data) => {
if (e) {
console.error(e);
} else {
db.run('INSERT INTO State (phone, state) VALUES ($phone, $state)', {
$phone: parseInt(phone),
$state: states.getUsername
}, (err) => {
if (err) {
console.error(err);
}
});
}
}
);
res.send({});
});
La fonction suivante dans le processus, setUsername montre une transition d'état complète. Comme l'étape précédente a envoyé une invite, on suppose que le message suivant qui revient est la réponse. Par conséquent, le texte du message est inséré dans la table Users en tant que nom d'utilisateur du nouvel utilisateur. Une fois que c'est fait, le reste est comme le point de terminaison /signup point final. Le serveur envoie l'invite suivante et met à jour la table State table :
function setUsername(phone, username) {
db.run('INSERT INTO Users (phone, username) VALUES ($phone, $username)', {
$phone: parseInt(phone),
$username: username
}, (err, row) => {
if (err) {
console.error(err);
}
});
vonage.channel.send(
{ type: 'whatsapp', number: phone },
{ type: 'whatsapp', number: process.env.WHATSAPP_NUM },
{ content: {
type: 'text',
text: 'Nice to meet you, ' + username + '! What\'s your email address?'
}}, (e, data) => {
if (e) {
console.error(e);
} else {
db.run('UPDATE State SET state = $state WHERE phone = $phone', {
$phone: parseInt(phone),
$state: states.getEmail
}, (err, row) => {
if (err) {
console.error(err);
}
});
}
}
);
}
Prochaines étapes
Si votre robot répond principalement à des questions, vous n'avez peut-être pas besoin d'une machine à états et le codage en dur de quelques fonctions peut être la chose la plus judicieuse à faire. Mais vous avez peut-être remarqué que les flux de travail tels que l'inscription de l'utilisateur dans l'exemple utilisent des informations légèrement différentes pour les mêmes tâches à chaque étape. En abstrayant les éléments communs dans une seule fonction et en fournissant un ensemble plus détaillé d'états, vous pouvez demander au serveur de faire avancer le processus d'une manière moins manuelle. Pour notre exemple, une définition élargie d'un état pourrait inclure :
nom de la colonne à mettre à jour
invite suivante
État suivant
Vous pouvez ajouter d'autres informations, comme des noms de tables, pour gérer les états dans plusieurs processus différents.
Il existe toutes sortes de façons intéressantes de structurer un robot de messagerie. Consultez la documentation de documentation de l'API Messages de Vonage pour en savoir plus sur les fonctionnalités et les cas d'utilisation qui peuvent être utiles à votre projet.
Partager:
Je suis développeur JavaScript et éducateur de développeurs chez Vonage. Au fil des ans, j'ai été très intéressé par les modèles, Node.js, les applications Web progressives et les stratégies offline-first, mais ce que j'ai toujours aimé, c'est une API utile et bien documentée. Mon objectif est de faire en sorte que votre expérience de l'utilisation de nos API soit la meilleure possible.
