
Creación de chat de grupo de mensajes de texto con la API SMS de Nexmo y PHP
Para ejercitar un poco la nueva librería cliente PHP, vamos a construir un simple chat de grupo SMS donde el mensaje entrante de un usuario es enviado a todos los otros miembros del chat. Puedes seguirme y construirlo conmigo, o simplemente clonar el repositorio Nexmo SMS Group Chat y verlo en acción.
No juzgaré si simplemente clonas el repo, de verdad, no lo haré. Mucho.
Qué estamos construyendo
Vamos a armar un simple script que:
Permite a los usuarios escribir
JOINy su nombre (por ejemploJOIN tlytle) a un número de teléfono, donde el número de teléfono representa un "grupo".Una vez unido, cualquier mensaje que envíe un usuario será retransmitido al resto del grupo. Y los usuarios reciben cualquier mensaje que envíe otra persona.
Si un usuario decide que ya no quiere formar parte del grupo, el envío de
LEAVEle dará de baja.
En un post posterior también crearemos una sencilla interfaz web en la que podrán ver un registro de los mensajes del grupo.
Puesta en marcha
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.
Empezaremos con la librería cliente Nexmoy una base de datos Mongo. Configurar la base de datos está más allá del alcance de este tutorial, pero hay algunos hosts Mongo con niveles gratuitos, y configurar una base de datos en uno de ellos debería ser bastante sencillo. También necesitarás el controlador controlador Mongo para PHP instalado.
Puede incluir eso y la biblioteca de clientes Nexmo con Composer:
$ composer require nexmo/client 1.0.*@beta
$ composer require mongodb/mongodbDefinir un archivo de configuración simple nos permitirá mantener nuestras credenciales de la API y la conexión a la base de datos en un único archivo.
He aquí un ejemplo config.php para usar como plantilla:
<?php
return [
'mongo' => [
'uri' => 'mongodb://user:password@host:port',
'database' => 'groupchat'
],
'nexmo' => [
'key' => 'key',
'secret' => 'secret'
]
];Un archivo bootstrap (bootstrap.php) se encarga de la carga automática y transmite el archivo de configuración:
<?php
$autoloader = require __DIR__ . '/vendor/autoload.php';
$config = require __DIR__ . '/config.php';
$config['autoloader'] = $autoloader;
return $config;Con estos dos archivos en su lugar, estamos listos para construir nuestra aplicación de chat de grupo.
Gestión de los mensajes de texto entrantes
Necesitaremos un script que acepte webhooks entrantes de Nexmo, y procese el mensaje. Por lo tanto, crear un public/inbound.php y dentro incluye ../bootstrap.phpcrea un cliente Nexmo, y un cliente 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']);A continuación, queremos crear un mensaje de entrada a partir de una solicitud de entrada y comprobar que es válido. La biblioteca cliente proporciona una forma sencilla de hacerlo.
$inbound = \Nexmo\Message\InboundMessage::createFromGlobals();
if(!$inbound->isValid()){
error_log('not an inbound message');
return;
}Ahora que el archivo está configurado puede dirigirse al panel de Nexmoy apuntar un número a ese script en el campo Callback URL campo

Eso configura su cuenta Nexmo para hacer una solicitud webhook a la secuencia de comandos cada vez que se envía un mensaje a ese número. Si estás desarrollando localmente, necesitarás usar algo como ngrok para crear un túnel local con una URL pública que la plataforma Nexmo pueda alcanzar.
Una vez configurado el número, puedes enviarle un mensaje, pero no hará nada. Así que vamos a responder al remitente con algunas instrucciones. El objeto de mensaje entrante creado por la biblioteca cliente tiene un método createReply que utiliza los datos del webhook entrante para crear una respuesta volteando los botones to y el campo from.
$nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));Ahora envía un mensaje a tu número Nexmo y recibirás una respuesta rápida.
Como estamos utilizando los parámetros enviados con el webhook entrante, nuestro código no necesita saber qué número utilizar como remitente. ¿Por qué es importante? Ahora sin código adicional o configuración tenemos una simple autorespuesta que soporta tantos números Nexmo - y por extensión chats de grupo - como le indiquemos.
Comandos de procesamiento
Antes de poder procesar JOIN comandos, necesitamos saber si el usuario ya ha interactuado con el sistema. Así que es hora de escribir algunas consultas. Prepararemos las cosas con una colección users colección. Y esperaremos que cada documento tenga la propiedad group con el número Nexmo de entrada al que se envió el mensaje. El user se establecerá en el número del usuario (el número desde el que se envió el mensaje).
$user = $db->selectCollection('users')->findOne([
'group' => $inbound->getTo(), // the group's number
'user' => $inbound->getFrom() //the user's number
]);Vamos a añadir un registro de errores simple para que podamos solucionar problemas si es necesario:
if($user){
error_log('found user: ' . $user['name']);
} else {
error_log('no user found');
}Dado que no hay datos en la base de datos, cualquier mensaje en este punto debe registrar no user found. Ahora que tenemos el código en su lugar para la comprobación de uso, podemos empezar a buscar palabras clave de comandos.
Utilizaremos la primera palabra para comprobar si el usuario está enviando un comando. Dado que el comando JOIN también espera un nombre, necesitamos analizar el mensaje en un único comando como primera palabra, y un argumento opcional después. Usando una expresión regular para dividir en cualquier espacio, y limitando eso a 2 elementos nos da lo que necesitamos. Con un comando analizado, un switch nos permitirá actuar sobre la primera palabra:
$command = preg_split('#\s+#', $inbound->getBody(), 2);
switch(strtolower(trim($command[0]))){Para empezar, comprobemos si también se ha proporcionado el segundo argumento esperado, al menos para los nuevos usuarios. Si no es así, enviar una respuesta es fácil, simplemente moveremos la respuesta que ya tenemos aquí:
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;
}Si se trata de un usuario nuevo (no se ha encontrado ningún usuario existente), y ha proporcionado un nombre ($command['1'] no era empty()) debemos configurar los datos básicos del usuario:
if(!$user){
$user = [
'group' => $inbound->getTo(),
'user' => $inbound->getFrom(),
'actions' => []
];
}Y no olvidemos el nombre. ¿Por qué lo hacemos fuera de la comprobación de nuevo usuario? Para permitir que un usuario existente actualice su nombre utilizando el comando JOIN si proporciona uno nuevo. Dado que nos estamos asegurando de que los nuevos usuarios tienen ese segundo argumento, sabemos que cualquier nuevo usuario tendrá el nombre establecido también:
if(isset($command[1])){
$user['name'] = $command[1];
}Como se trata de un comando JOIN también tenemos que establecer el estado del usuario en activo y crear una entrada de registro para la acción.
$user['status'] = 'active';
$user['actions'][] = [
'command' => 'join',
'date' => new \MongoDB\BSON\UTCDatetime(microtime(true))
];Ahora sólo tenemos que guardar (o crear) el usuario. Usaremos el comando replaceOne y haremos que inserte el documento (upsert) si es necesario, y añadiremos break para que dejemos de procesar una vez realizada la acción:
$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;JOINnos lleva a mitad de camino, pero todavía tenemos que permitir a los usuarios a LEAVE a un grupo. Como en JOIN haremos un poco de registro y comprobaremos que el usuario está realmente suscrito - realmente no pueden salir si no lo están. Si no están suscritos, sólo responderemos con un poco de ayuda. Lo cual, como hemos descubierto, es bastante fácil de hacer:
case 'leave';
error_log('got leave command');
if(!$user){
$nexmo->message()->send($inbound->createReply('Use JOIN [your name] to join this group.'));
break;
}Si se están suscribiendo, tenemos que actualizar el estado de la suscripción y registrar que se ha realizado la acción. Esto se hace cambiando la propiedad status y añadiendo un nuevo miembro al array actions array. Por supuesto, escribir este cambio en la base de datos también es importante:
//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);Una vez que hemos eliminado al usuario del grupo, debemos informarle de que se ha ido y de cómo puede volver a unirse en el futuro:
//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 grupo SMS por retransmisión de mensajes
Una vez solucionado el problema de entrar y salir del grupo, ahora tenemos que gestionar el envío de un mensaje por parte de un usuario, no de una orden. Cualquier mensaje que no sea un comando es un mensaje al grupo. La lógica aquí es simple, si el usuario está suscrito y activo, su mensaje debe ser enviado a todos los otros miembros de miembros.
Tenemos que comprobar que el usuario puede publicar un mensaje en el grupo. Si encontramos un usuario en la base de datos, significa que en algún momento estuvo suscrito al grupo, pero tenemos que comprobar si se ha dado de baja. Si cualquiera de los dos casos no es cierto -no se encuentran en la base de datos, o no están activos en el grupo- enviaremos una respuesta rápida de ayuda:
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;
}Si están suscritos y activos, creamos un archivo de su mensaje. Éste contiene el texto, el grupo al que lo envió, el propio usuario (así como su nombre, para evitar tener que buscar el usuario cada vez que se necesite el nombre) y otros metadatos.
También crearemos un array sends vacío para registrar los mensajes enviados a los demás usuarios del grupo:
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' => []
];Para encontrar a todos los miembros que necesitan que se retransmita el mensaje, consultamos la colección users en busca de los usuarios de este grupo que estén marcados como activos. Tenemos que acordarnos de excluir al usuario actual (eso es lo que significa el símbolo $ne significa 'no igual'), pero puede ser útil eliminarlo para realizar pruebas:
$members = $db->selectCollection('users')->find([
'group' => $inbound->getTo(),
'user' => ['$ne' => $inbound->getFrom()],
'status' => 'active'
]);Una vez que tenemos esa lista podemos iterar sobre ella y enviar un mensaje a cada miembro. Podemos pasar un simple array al método send() (así como un objeto Message ). Ese array usa el número del miembro como el valor toel número del grupo como el campo fromy añadiremos el nombre del usuario que ha enviado el mensaje al campo text antes de enviar el mensaje.
Esto devolverá un objeto mensaje completo. Podríamos tratarlo como un array, pero es más fácil utilizar los métodos getter para añadir el id del mensaje y el número del miembro al registro de envío.
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()
];
}Una vez enviados todos los mensajes, añadimos el nuevo mensaje a la colección de registros de la base de datos, y hemos terminado de procesar los mensajes entrantes.
$db->selectCollection('logs')->insertOne($log);
error_log('relayed message');
break;
} // end of switch Próximos pasos
Y con eso hemos configurado un simple script que acepta mensajes entrantes, responde a algunos de ellos, y retransmite otros a un grupo. De forma centralizada, el concepto de comando podría ampliarse a bots de respuesta automática más complejos e interactivos, la retransmisión de grupo podría convertirse en un proxy de dos usuarios que solo enmascare los números de los usuarios, o esto podría reutilizarse como una lista de distribución de SMS que permita a cualquiera enviar un mensaje entrante a un grupo de personas.

Dondequiera que lo lleve, el procesamiento de mensajes entrantes y el envío de mensajes salientes es una tarea fácil con la biblioteca de cliente PHP y la API de Nexmo.
También hay algo más en esta demo (que puedes clonar y ejecutar si quieres), y construiremos una interfaz web para nuestro chat de grupo en la segunda parte de este tutorial.
