https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-text-message-group-chat-nexmo-sms-api-php-dr/group-chat-sms-terminal.png

Construire un chat de groupe par message texte avec l'API SMS Nexmo et PHP

Publié le May 13, 2021

Temps de lecture : 8 minutes

Pour exercer un peu la nouvelle bibliothèque client PHP, nous allons construire un simple chat de groupe SMS où le message entrant d'un utilisateur est envoyé à tous les autres membres du chat. Vous pouvez suivre ici et le construire avec moi, ou simplement cloner le référentiel Nexmo SMS Group Chat et le voir en action.

Je ne jugerai pas si vous clonez simplement le repo, vraiment, je ne jugerai pas. Beaucoup.

Ce que nous construisons

Nous allons mettre au point un script simple qui :

  • Permet aux utilisateurs d'écrire JOIN et leur nom (par exemple JOIN tlytle) à un numéro de téléphone, ce dernier représentant un "groupe".

  • Une fois le groupe rejoint, tout message envoyé par un utilisateur sera relayé au reste du groupe. Les utilisateurs reçoivent également tout message envoyé par un autre utilisateur.

  • Si un utilisateur décide de ne plus faire partie du groupe, sending LEAVE le désabonnera.

Dans un article ultérieur, nous mettrons également en place une interface web simple qui leur permettra de consulter un journal des messages du groupe.

Mise en place

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.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Nous commencerons par la bibliothèque client Nexmoet une base de données Mongo. La configuration de la base de données dépasse le cadre de ce tutoriel, mais il existe quelques hébergeurs Mongo avec des niveaux gratuits, et la configuration d'une base de données sur l'un d'entre eux devrait être assez simple. Vous aurez également besoin du pilote Mongo driver for PHP installé.

Vous pouvez inclure cette bibliothèque et la bibliothèque client Nexmo dans Composer :

$ composer require nexmo/client 1.0.*@beta
 $ composer require mongodb/mongodb

La définition d'un simple fichier de configuration nous permettra de conserver nos identifiants API et notre connexion à la base de données dans un seul fichier.

Voici un exemple config.php à utiliser comme modèle :

<?php
return [
    'mongo' => [
        'uri' => 'mongodb://user:password@host:port',
        'database' => 'groupchat'
    ],
    'nexmo' => [
        'key' => 'key',
        'secret' => 'secret'
    ]
];

Un fichier d'amorçage très simple (bootstrap.php) très simple se charge de l'autoloading et transmet le fichier de configuration :

<?php
$autoloader = require __DIR__ . '/vendor/autoload.php';
$config = require  __DIR__ . '/config.php';
$config['autoloader'] = $autoloader;

return $config;

Avec ces deux fichiers en place, nous sommes prêts à construire notre application de chat de groupe.

Traitement des messages textuels entrants

Nous aurons besoin d'un script qui accepte les webhooks entrants de Nexmo et traite le message. Donc, créez un fichier public/inbound.php et à l'intérieur de celui-ci inclure ../bootstrap.phpcréez un client Nexmo et un client Mongo.

<?php
$config = require __DIR__ . '/../bootstrap.php';
$nexmo = new \Nexmo\Client(new \Nexmo\Client\Credentials\Basic($config['nexmo']['key'], $config['nexmo']['secret']));
$mongo = new \MongoDB\Client($config['mongo']['uri']);
$db = $mongo->selectDatabase($config['mongo']['database']);

Ensuite, nous voulons créer un message entrant à partir d'une requête entrante et vérifier qu'il est valide. La bibliothèque client fournit un moyen simple de le faire.

$inbound = \Nexmo\Message\InboundMessage::createFromGlobals();
if(!$inbound->isValid()){
    error_log('not an inbound message');
    return;
}

Maintenant que le fichier est configuré, vous pouvez vous rendre sur le tableau de bord Nexmoet pointer un numéro vers ce script dans le champ Callback URL champ.

Phone Number SMS Callback Webhook Settings

Cela configure votre compte Numbers pour qu'il envoie une requête webhook au script chaque fois qu'un message est envoyé à ce numéro. Si vous développez localement, vous devrez utiliser quelque chose comme ngrok pour créer un tunnel local avec une URL publique que la plateforme Nexmo peut atteindre.

Une fois le numéro configuré, vous pouvez lui envoyer un message - mais il ne fera rien. Répondons donc à l'expéditeur en lui donnant des instructions. L'objet message entrant créé par la bibliothèque client possède une méthode createReply qui utilise les données du webhook entrant pour créer une réponse en inversant l'élément to et le from.

$nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));

Envoyez ensuite un message à votre numéro Numbers, et vous devriez recevoir une réponse rapide.

Comme nous utilisons les paramètres envoyés avec le webhook entrant, notre code n'a pas besoin de savoir quel numéro utiliser comme expéditeur. Pourquoi est-ce important ? Maintenant, sans code ou configuration supplémentaire, nous avons un répondeur automatique simple qui prend en charge autant de numéros Nexmo - et par extension de chats de groupe - que nous lui indiquons.

Traitement des commandes

Avant de pouvoir traiter une commande, nous devons savoir si l'utilisateur a déjà interagi avec le système. JOIN nous devons savoir si l'utilisateur a déjà interagi avec le système. Il est donc temps d'écrire quelques requêtes. Nous allons mettre les choses en place avec une collection users collection. Et nous nous attendrons à ce que chaque document ait la propriété group soit définie comme le numéro de Numbers entrant auquel le message a été envoyé. La propriété user sera définie comme étant le numéro de l'utilisateur (le numéro à partir duquel le message a été envoyé).

$user = $db->selectCollection('users')->findOne([
    'group' => $inbound->getTo(), // the group's number
    'user'  => $inbound->getFrom() //the user's  number
]);

Ajoutons un journal d'erreurs simple afin de pouvoir résoudre les problèmes si nécessaire :

if($user){
    error_log('found user: ' . $user['name']);
} else {
    error_log('no user found');
}

Étant donné qu'il n'y a pas de données dans la base de données, tout message à ce stade devrait enregistrer no user found. Maintenant que nous avons mis en place un code pour la vérification de l'utilisation, nous pouvons commencer à rechercher les mots-clés de la commande.

Nous utiliserons le premier mot pour vérifier si l'utilisateur envoie une commande. Comme la commande JOIN attend également un nom, nous devons analyser le message en une seule commande comme premier mot, et un argument optionnel ensuite. L'utilisation d'une expression régulière pour couper tout espace, et la limitation à 2 éléments nous donne ce dont nous avons besoin. Avec une commande analysée, un switch nous permet d'agir sur le premier mot :

$command = preg_split('#\s+#', $inbound->getBody(), 2);
switch(strtolower(trim($command[0]))){

Pour commencer, vérifions si le deuxième argument attendu a également été fourni - au moins pour les nouveaux utilisateurs. Si ce n'est pas le cas, il est facile d'envoyer une réponse, nous allons simplement déplacer la réponse que nous avons déjà ici :

case 'join';
    error_log('got join command');

    if(!$user && empty($command[1])){
        $nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));
        break;
    }

S'il s'agit d'un nouvel utilisateur (aucun utilisateur existant n'a été trouvé) et qu'il a fourni un nom ($command['1'] n'était pas empty()), nous devons configurer les données de base de l'utilisateur :

if(!$user){
    $user = [
        'group' => $inbound->getTo(),
        'user' => $inbound->getFrom(),
        'actions' => []
    ];
}

Et n'oublions pas ce nom. Pourquoi le faisons-nous en dehors la vérification du nouvel utilisateur ? Pour permettre à un utilisateur existant de mettre à jour son nom à l'aide de la commande JOIN s'il en fournit un nouveau. Puisque nous nous assurons que les nouveaux utilisateurs ont ce deuxième argument, nous savons que tout nouvel utilisateur aura également le nom défini :

if(isset($command[1])){
    $user['name'] = $command[1];
}

Comme il s'agit d'une JOIN nous devons également définir le statut de l'utilisateur comme actif et créer une entrée de journal pour l'action.

$user['status'] = 'active';
$user['actions'][] = [
    'command' => 'join',
    'date' => new \MongoDB\BSON\UTCDatetime(microtime(true))
];

Il ne nous reste plus qu'à enregistrer (ou créer) l'utilisateur. Nous utiliserons la commande replaceOne de Mongo et lui demander d'insérer le document (upsert) si nécessaire, et ajouter break afin d'arrêter le traitement une fois l'action effectuée :

$db->selectCollection('users')->replaceOne([
    'group' => $inbound->getTo(), // the group's number
    'user'  => $inbound->getFrom() //the user's  number
], $user, ['upsert' => true]);

error_log('added user');
break;

JOINNous avons fait la moitié du chemin, mais nous devons encore autoriser les utilisateurs à faire partie d'un groupe. LEAVE un groupe. Comme JOIN nous allons faire un peu de logging et vérifier que l'utilisateur est bien abonné - il ne peut pas vraiment partir s'il ne l'est pas. S'il n'est pas abonné, nous lui répondrons simplement en l'aidant. Ce qui, comme nous l'avons constaté, est assez facile à faire :

case 'leave';
    error_log('got leave command');

    if(!$user){
        $nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));
        break;
    }

S'ils s'abonnent, nous devons mettre à jour l'état de l'abonnement et indiquer que l'action a été effectuée. Pour ce faire, nous modifions la propriété status et en ajoutant un nouveau membre au tableau actions un nouveau membre. Bien entendu, il est également important d'écrire ce changement dans la base de données :

//update the user's status
$user['status'] = 'inactive';
$user['actions'][] = [
    'command' => 'leave',
    'date' => new \MongoDB\BSON\UTCDatetime(microtime(true))
];

//update the database
$db->selectCollection('users')->replaceOne([
    'group' => $inbound->getTo(), // the group's number
    'user'  => $inbound->getFrom() //the user's  number
], $user);

Une fois que l'utilisateur a été retiré du groupe, nous devons lui faire savoir qu'il l'a quitté et lui indiquer comment il peut le rejoindre à nouveau à l'avenir :

//let them know they've left
$nexmo->message()->send($inbound->createReply('You have left. Use JOIN to join this group again.'));

error_log('removed user');
break;

Chat de groupe par SMS en relayant les messages

L'entrée et la sortie du groupe étant réglées, nous devons maintenant gérer l'envoi d'un message par un utilisateur, et non d'une commande. Tout message qui n'est pas une commande est un message au groupe. La logique est simple : si l'utilisateur est abonné et actif, son message doit être envoyé à tous les autres membres.

Nous devons vérifier que l'utilisateur est en mesure de publier un message dans le groupe. Si nous avons trouvé un utilisateur dans la base de données, cela signifie qu'il a été abonné au groupe à un moment donné, mais nous devons vérifier s'il l'a quitté. Si l'un ou l'autre de ces cas n'est pas vrai - l'utilisateur n'est pas trouvé dans la base de données ou il n'est pas actif dans le groupe - nous enverrons une réponse rapide et utile :

default:
    error_log('no command found');

    if(!$user || 'active' != $user['status']){
        $nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));
        break;
    }

S'il est abonné et actif, nous créons une archive de son message. Celle-ci contient le texte, le groupe auquel il l'a envoyé, l'utilisateur lui-même (ainsi que son nom, pour éviter d'avoir à rechercher l'utilisateur chaque fois que le nom est nécessaire), et d'autres métadonnées.

Nous allons également créer un tableau sends pour enregistrer les messages envoyés aux autres utilisateurs du groupe :

error_log('user is active');

$log = [
    '_id'   => $inbound->getMessageId(),
    'text'  => $inbound->getBody(),
    'date'  => new \MongoDB\BSON\UTCDatetime(microtime(true)),
    'group' => $inbound->getTo(),
    'user'  => $inbound->getFrom(),
    'name'  => $user['name'],
    'sends' => []
];

Pour trouver tous les membres qui ont besoin que le message soit relayé, nous interrogeons la collection pour trouver tous les utilisateurs de ce groupe spécifique qui sont marqués comme actifs. users pour trouver tous les utilisateurs de ce groupe spécifique qui sont marqués comme actifs. Nous devons nous rappeler d'exclure l'utilisateur actuel (c'est ce que signifie l'attribut $ne signifie "pas égal"), mais il peut être utile de le supprimer à des fins de test :

$members = $db->selectCollection('users')->find([
    'group'  => $inbound->getTo(),
    'user'   => ['$ne' => $inbound->getFrom()],
    'status' => 'active'
]);

Une fois que nous avons cette liste, nous pouvons la parcourir et envoyer un message à chaque membre. Nous pouvons passer un simple tableau à la méthode send() (ainsi qu'un objet Message ). Ce tableau utilise le numéro du membre comme l'élément to, le numéro du groupe comme l'élément fromet nous ajouterons le nom de l'utilisateur qui a posté le message à l'élément text avant d'envoyer le message.

Cela renverra un objet message complet. Nous pourrions le traiter comme un tableau, mais il est plus facile d'utiliser les méthodes de récupération pour ajouter l'identifiant du message et le numéro du membre au journal d'envoi.

foreach($members as $member) {
    $sent = $nexmo->message()->send([
        'to'   => $member['user'],
        'from' => $inbound->getTo(),
        'text' => $user['name'] . ': ' . $inbound->getBody()
    ]);

    $log['sends'][] = [
        'user' => $sent->getTo(),
        'id'   => $sent->getMessageId()
    ];
}

Une fois que tous les messages ont été envoyés, nous ajoutons le nouveau message à la collection de journaux de la base de données, et nous avons terminé le traitement des messages entrants.

    $db->selectCollection('logs')->insertOne($log);

    error_log('relayed message');
    break;
} // end of switch

Prochaines étapes

Nous avons donc mis en place un script simple qui accepte les messages entrants, répond à certains d'entre eux et relaie les autres à un groupe. Au niveau central, le concept de commande pourrait être étendu à des robots répondeurs automatiques plus complexes et interactifs, le relais de groupe pourrait être transformé en deux proxy d'utilisateur qui ne masqueraient que les numéros de l'utilisateur, ou il pourrait être reconverti en liste de distribution SMS qui permettrait à n'importe qui d'envoyer un message entrant à un groupe de personnes.

Group SMS Chat in the terminal

Où que vous le preniez, le traitement des messages entrants et l'envoi des messages sortants est une tâche facile avec la bibliothèque client PHP et l'API de Nexmo.

Cette démo est un peu plus complète (vous pouvez la cloner et l'exécuter si vous le souhaitez), et nous construirons une interface web pour notre chat de groupe dans la deuxième partie de ce tutoriel.

Ressources utiles

Partager:

https://a.storyblok.com/f/270183/384x384/f71222cc5f/tjlytle.png
Tim LytleAnciens de Vonage