https://a.storyblok.com/f/270183/320556/4f1449b359/group-chat-sms-terminal.png

Como criar um chat em grupo por mensagens de texto com a SMS API da Nexmo e PHP

Publicado em May 13, 2021

Tempo de leitura: 8 minutos

Para testar um pouco a nova biblioteca cliente do PHP, vamos criar um chat em grupo por SMS simples, no qual a mensagem recebida por um usuário é enviada a todos os outros participantes do chat. Você pode acompanhar aqui e criar o projeto comigo, ou simplesmente clonar o repositório do Chat em Grupo por SMS da Nexmo e ver como funciona.

Não vou te julgar se você simplesmente clonar o repositório, sério, não vou. Não muito, pelo menos.

O que estamos construindo

Vamos criar um script simples que:

  • Permite que os usuários enviem mensagens de texto JOIN e seu nome (por exemplo, JOIN tlytle) para um número de telefone, em que esse número representa um “grupo”.

  • Depois de entrar no grupo, qualquer mensagem enviada por um usuário será encaminhada para o restante do grupo. E os usuários recebem todas as mensagens enviadas por outras pessoas.

  • Se um usuário decidir que não quer mais fazer parte do grupo, basta enviar LEAVE fará com que ele seja cancelado da lista.

Em uma postagem futura, também criaremos uma interface web simples onde eles poderão ver um registro das mensagens do grupo.

Configuração

Vamos começar com a biblioteca de clientes da Nexmoe um banco de dados Mongo. A configuração do banco de dados está fora do escopo deste tutorial, mas existem alguns provedores de Mongo que oferecem planos gratuitos, e configurar um banco de dados em um deles deve ser bastante simples. Você também precisará do driver do Mongo para PHP instalado.

Você pode incluir isso e a biblioteca do cliente Nexmo usando o Composer:

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

A definição de um arquivo de configuração simples nos permitirá manter nossas credenciais de API e a conexão com o banco de dados em um único arquivo.

Aqui está um exemplo config.php para usar como modelo:

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

Um arquivo de inicialização muito simples (bootstrap.php) cuida do carregamento automático e repassa o arquivo de configuração:

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

return $config;

Com esses dois arquivos prontos, estamos prontos para desenvolver nosso aplicativo de bate-papo em grupo.

Tratamento de mensagens de texto recebidas

Vamos precisar de um script que aceite webhooks de entrada da Nexmo e processe a mensagem. Então, crie um public/inbound.php e, dentro dele, inclua ../bootstrap.php, crie um cliente Nexmo e um cliente MongoDB.

<?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']);

Em seguida, queremos criar uma mensagem de entrada a partir de uma solicitação de entrada e verificar se ela é válida. A biblioteca do cliente oferece uma maneira simples de fazer isso.

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

Agora que o arquivo está configurado, você pode acessar o painel da Nexmoe associar um número a esse script no Callback URL campo.

Phone Number SMS Callback Webhook Settings

Isso configura sua conta do Nexmo para enviar uma solicitação de webhook ao script sempre que uma mensagem for enviada para esse número. Se você estiver desenvolvendo localmente, precisará usar algo como ngrok para criar um túnel local com uma URL pública que a plataforma Nexmo possa acessar.

Depois de configurar o número, você pode enviar uma mensagem para ele — mas isso não vai surtir efeito. Então, vamos responder ao remetente com algumas instruções. O objeto de mensagem recebida criado pela biblioteca do cliente possui um createReply método que usa os dados do webhook de entrada para criar uma resposta invertendo o to e o from.

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

Agora envie uma mensagem para o seu número da Nexmo e você deve receber uma resposta rápida e simpática.

Como estamos usando os parâmetros enviados com o webhook de entrada, nosso código não precisa saber qual número usar como remetente. Por que isso é importante? Agora, sem nenhum código ou configuração adicional, temos um respondedor automático simples que suporta quantos números da Nexmo — e, por extensão, quantos chats em grupo — quisermos indicar a ele.

Comandos de processamento

Antes de podermos realmente processar qualquer JOIN comandos, precisamos saber se o usuário já interagiu com o sistema. Então, é hora de escrever algumas consultas. Vamos configurar tudo com uma users coleção. E esperamos que cada documento tenha a group propriedade definida como o número da Nexmo de destino para o qual a mensagem foi enviada. A user será definido como o número do usuário (o número de onde a mensagem foi enviada).

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

Vamos adicionar um registro de erros simples para que possamos solucionar problemas, se necessário:

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

Como não há dados no banco de dados, qualquer mensagem neste momento deve ser registrada no user found. Agora que já temos o código pronto para a verificação de uso, podemos começar a procurar por palavras-chave de comando.

Usaremos a primeira palavra para verificar se o usuário está enviando um comando. Como o JOIN comando também espera um nome, precisamos analisar a mensagem em um único comando como a primeira palavra, seguido por um argumento opcional. Usando uma expressão regular para dividir em qualquer espaço e limitando isso a dois elementos, obtemos o que precisamos. Com o comando analisado, um switch nos permitirá agir sobre essa primeira palavra:

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

Para começar, vamos verificar se o segundo argumento esperado também foi fornecido — pelo menos para novos usuários. Se não for o caso, enviar uma resposta é fácil: basta copiar a resposta que já temos aqui:

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;
    }

Se for um novo usuário (não foi encontrado nenhum usuário existente) e ele tiver fornecido um nome ($command['1'] não foi empty()), devemos configurar os dados básicos do usuário:

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

E não vamos esquecer esse nome. Por que fazemos isso fora a verificação de novos usuários? Para permitir que um usuário existente atualize seu nome usando o JOIN , caso forneça um novo. Como estamos garantindo que novos usuários tenham esse segundo argumento, sabemos que qualquer novo usuário também terá o nome definido:

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

Como se trata de um JOIN comando, também precisamos definir o status do usuário como ativo e criar uma entrada no log para a ação.

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

Agora só precisamos salvar (ou criar) o usuário. Vamos usar o comando replaceOne e fazer com que ele insira o documento (upsert) se necessário, e adicionar break para interromper o processamento assim que a ação for realizada:

$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;

JOINIsso já nos leva até a metade do caminho, mas ainda precisamos permitir que os usuários LEAVE um grupo. Por exemplo, JOIN vamos fazer um pequeno registro e verificar se o usuário está realmente inscrito — ele não pode sair do grupo se não estiver. Se ele não estiver inscrito, vamos apenas responder com algumas orientações. O que, como descobrimos, é bem fácil de fazer:

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

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

Se o usuário estiver se inscrevendo, precisamos atualizar o status da inscrição e registrar que a ação foi realizada. Isso é feito alterando a status propriedade e acrescentando um novo membro à actions array. É claro que gravar essa alteração no banco de dados também é 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);

Depois de removemos o usuário do grupo, devemos informá-lo de que ele saiu e explicar como poderá voltar a participar no 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;

Bate-papo em grupo por SMS por meio do reencaminhamento de mensagens

Depois de resolvermos a questão de entrar e sair do grupo, agora precisamos lidar com o caso de um usuário enviar uma mensagem, e não um comando. Qualquer mensagem que não seja um comando é considerada uma mensagem para o grupo. A lógica aqui é simples: se o usuário estiver inscrito e ativo, sua mensagem deve ser enviada a todos os outros membros.

Precisamos verificar se o usuário pode postar uma mensagem no grupo. Se encontrarmos um usuário no banco de dados, isso significa que ele já foi inscrito no grupo em algum momento, mas precisamos verificar se ele saiu. Se nenhuma das duas condições for verdadeira — ou ele não for encontrado no banco de dados, ou não estiver ativo no grupo —, enviaremos uma resposta rápida e útil:

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;
    }

Se estiverem inscritos e ativos, criamos um arquivo com a mensagem deles. Esse arquivo contém o texto, o grupo para o qual a mensagem foi enviada, o próprio usuário (bem como seu nome, para evitar ter que procurar o usuário toda vez que o nome for necessário) e outros metadados.

Também vamos criar um sends para registrar as mensagens enviadas aos outros usuários do 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 todos os membros que precisam receber a mensagem, consultamos a users coleção em busca de usuários nesse grupo específico que estejam marcados como ativos. Precisamos nos lembrar de excluir o usuário atual (é isso que $ne significa, “não igual”), mas pode ser útil remover essa condição para fins de teste:

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

Assim que tivermos essa lista, podemos percorrê-la e enviar uma mensagem a cada membro. Podemos passar um array simples para o send() método (assim como um Message objeto). Esse array usa o número do membro como to, o número do grupo como o from, e adicionaremos o nome do usuário que postou a mensagem ao text antes de enviar a mensagem.

Isso retornará um objeto de mensagem completo. Poderíamos tratá-lo como uma matriz, mas é mais fácil simplesmente usar os métodos getter para adicionar o ID da mensagem e o número do membro ao registro de envio.

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()
    ];
}

Depois que todas as mensagens forem enviadas, adicionamos a nova mensagem à coleção de logs no banco de dados, e concluímos o processamento das mensagens recebidas.

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

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

Próximos passos

E com isso, criamos um script simples que aceita mensagens recebidas, responde a algumas delas e encaminha outras para um grupo. De maneira geral, o conceito de comando poderia ser ampliado para bots de resposta automática mais complexos e interativos; o encaminhamento para o grupo poderia ser transformado em um proxy para dois usuários que apenas ocultasse os números dos usuários; ou isso poderia ser reaproveitado como uma lista de distribuição de SMS que permite que qualquer pessoa envie uma mensagem recebida para um grupo de pessoas.

Group SMS Chat in the terminal

Não importa onde você esteja, processar mensagens recebidas e enviar mensagens é uma tarefa fácil com a biblioteca cliente em PHP e a API da Nexmo.

Além disso, há um pouco mais nessa demonstração (que você pode simplesmente clonar e executar, se quiser), e vamos criar uma interface web para o nosso chat em grupo na segunda parte deste tutorial.

Recursos úteis

Compartilhar:

https://a.storyblok.com/f/270183/384x384/f71222cc5f/tjlytle.png
Tim LytleEx-funcionários da Vonage