Integrando seu chat com o Rocket Omnichannel – Parte 2

Integrando seu chat com o Rocket Omnichannel – Parte 2

Dando continuidade o nosso post anterior onde entendemos como utilizar as APIs do Rocket.Chat para manipular o Omnichannel, agora iremos integrar um chat funcional com essa ferramenta.

Irei partir do ponto do post anterior, onde já havia configurado o Rocket e habilitado o uso do Omnichannel. Agora colocando a mão na massa e vendo a integração ocorrer.

 

Aplicação inicial

Para o exemplo de chat, irei utilizar uma simples aplicação escrito com PHP + Swoole. Você pode visualiza-lá aqui: https://github.com/4linux/integrando-chat-rocket-omnichannel/tree/initial. Caso queria entender melhor sobre o seu funcionamento, pode olhar esse outro post que explica passo a passo como criar um chat como esse.

É um chat bem simples que basicamente recebe mensagens e replica para todos os usuários conectados. Não armazena nada em banco, não possui histórico de mensagens, salas ou qualquer funcionalidade do tipo. Se resume a um broadcast de mensagens.

Além disso acompanha no projeto um arquivo em HTML que representa o frontend do chat. Utiliza o a API de WebSocket do próprio JavaScript, tendo como unica biblioteca externa o bootstrap para melhorar um pouco a apresentação.

Para iniciar o chat primeiramente precisamos inicializar a pasta vendor para termos acesso ao autoload. Vou utilizar a imagem oficial do Swoole para facilitar seu uso. Com isso podemos executar o comando:

docker run --rm -v `pwd`:/var/www openswoole/swoole composer install

E para iniciar o chat podemos utilizar o comando:

docker run --rm -v `pwd`:/var/www --name swoole-chat -p "9502:9502" openswoole/swoole

Assim ele será disponibilizado na porta 9502. Para testar seu funcionamento, pode abrir o index.html em duas abas diferentes e visualizar seu funcionamento.

Com isso temos o chat funcionando. Simples, não? Mas agora que vem a integração.

Preparando o Rocket.Chat

Ao finalizar os passos do post anterior, já temos ele funcionando e com o Omnichannel habilitado. As APIs são habilitadas e públicas por padrão.

Precisamos fazer alguns pequenos ajustes em coisas que já fizemos. Primeiramente as chamadas de Webhook do Omnichannel precisam apontar para nossa aplicação. Ela ainda não está preparada para recebe-los e dar o tratamento necessários, mas já vamos ajustar isso.

Estou utilizando o docker para executar ambos aplicações e elas não se enxergam. Para ser o mais simples possível visto que ambas precisam se comunicar entre sim, vou criar um rede e adicionar as aplicações manualmente definindo um alias:

Com isso funcionando, posso ajustar o webhook dessa forma:

Basta definir a URL adicionando /webhook ao final, pois irei fazer com que o servidor responda nesse caminho.

Além disso defino para realizar chamados no eventos de chat aberto, encerrado e quando o atendente enviar mensagens.

Além disso preciso me adicionar como agente do Omnichannel para poder receber as mensagens:

E garantir que esteja online:

 

Se comunicando com o Rocket.Chat

A fim de adicionar um pouco de organização, vou começar a separar um pouco as responsabilidades.

Nesse primeiro momento irei criar uma classe para centralizar a lógica de chamadas de API ao Rocket.

Primeira definir a sua estrutura básica:

Nesse momento só estou criando um cliente de requisições HTTP do swoole.

Agora iremos definir o método para criação do visitante. A fim de facilitar, irei definir o token como sendo a chave da conexão websocket do Swoole (um número inteiro incremental) e seu nome também utilizando desse valor:

Agora precisamos de métodos para criar a fechar salas de conversa no Omnichannel:

E por último temos o método para realizar envio de mensagens:

Até então tudo bem simples. Uma classe encapsulando as chamadas de API que não possuem nenhuma lógica especial. Nenhuma regra de negócio complexa foi definida.

Com isso já temos toda a lógica de comunicação com o Rocket preparada.

Visualizar código completo

<?php

namespace FourLinux\ChatBackend;

use Swoole\Coroutine\Http\Client;

class Rocket
{

   private Client $client;

   public function __construct(string $host = 'rocketchat', int $port = 3000, bool $ssl = false)
   {
      $this->client = new Client($host, $port, $ssl);
   }

   public function createVisitor(int $fd): array
   {
      $this->client->setHeaders([
         'Content-Type' => 'application/json',
      ]);

      $data = [
         'visitor' => [
            'name' => "Visitante {$fd}",
            'token' => (string)$fd,
         ],
      ];
      $this->client->post('/api/v1/livechat/visitor', json_encode($data));
      $this->client->close();

      return [
         $this->client->getStatusCode(),
         $this->client->getBody(),
      ];
   }

   public function createRoom(string $visitorToken): array
   {
      $query = http_build_query([
         'token' => $visitorToken,
      ]);

      $this->client->get("/api/v1/livechat/room?{$query}");
      $this->client->close();

      return [
         $this->client->getStatusCode(),
         $this->client->getBody(),
      ];
   }

   public function closeRoom(string $roomId, string $visitorToken): array
   {
      $this->client->setHeaders([
         'Content-Type' => 'application/json',
      ]);

      $data = [
         'rid' => $roomId,
         'token' => $visitorToken,
      ];

      $this->client->post('/api/v1/livechat/room.close', json_encode($data));
      $this->client->close();

      return [
         $this->client->getStatusCode(),
         $this->client->getBody(),
      ];
   }

   public function sendMessage(string $visitorToken, string $roomId, string $message): array
   {
      $this->client->setHeaders([
         'Content-Type' => 'application/json',
      ]);

      $data = [
         'msg' => $message,
         'rid' => $roomId,
         'token' => $visitorToken,
      ];

      $this->client->post('/api/v1/livechat/message', json_encode($data));
      $this->client->close();

      return [
         $this->client->getStatusCode(),
         $this->client->getBody(),
      ];
   }

}

 

Tratando o retorno via Webhook

Agora vamos começar a tratar a chamada do webhook. Continuando a separação, vamos ter uma classe própria para processar essa requisição.

Para não complicar, vamos receber toda requisição HTTP e tratar no mesmo lugar.

O Swoole por padrão permite que um mesmo servidor suporte diferentes tipos de protocolos de comunicação, como HTTP, WS, TCP, etc., então não será necessário termos mais de uma aplicação em execução.

Primeiramente então vamos definir a estrutura base da classe:

Essa classe irá receber a instância do servidor e uma tabela onde iremos armazenar as conexões.

A instância é necessária para se comunicar com os clientes que estão conectados ao chat via websocket.

A tabela iremos utilizar para controlar a relação de quem está conectado.

Precisamos agora do método para processar a requisição. Ele irá receber as instâncias de request e response e deverá dar o tratamento necessário:

Primeiramente ela valida se o caminho da requisição é o /webhook. Caso negativo, já responde imediatamente com um erro.

Após isso, ele decodifica os dados da requisição e checa qual o tipo de evento ocorreu. Para cada um, chama um método específico para tratamento.

Em seguida, responde com uma mensagem OK.

Agora os métodos específicos de tratamento, vamos começar pelo de inicio de sessão (sala criada):

Ele basicamente vai enviar uma mensagem para o chat do usuário (cliente) informando que o chat foi iniciado (Sala criada no Rocket).

Ele utiliza de um método para auxiliar a resgatar o identificador do usuário e que será reaproveitado pelos outros eventos.

Temos também o método de envio de mensagem:

Não faz nada muito diferente do método anterior, apenas que nesse a mensagem a ser enviada é resgatada do que é recebido via webhook.

E temos o evento de encerramento (Sala fechada):

Ele segue a mesma ideia do inicio de sessão, onde é apenas enviado uma mensagem fixa informando o ocorrido.

Com isso temos todos os eventos de webhook esperados sendo tratados.

Agora vamos fazer o ajuste final no chat para realizar essa integração.

Visualizar código completo

<?php

namespace FourLinux\ChatBackend;

use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Table;
use Swoole\WebSocket\Server;

class RocketWebhookHandler
{

	private Server $server;

	private Table $connections;

	public function __construct(Server $server, Table $connections)
	{
		$this->server = $server;
		$this->connections = $connections;
	}

	public function handle(Request $request, Response $response): void
	{
		if ($request->server['path_info'] !== '/webhook') {
			$response->setStatusCode(404);
			$response->end();
			return;
		}

		$data = json_decode($request->getContent(), true);

		switch ($data['type']) {
			case 'LivechatSessionStart':
				$this->onSessionStart($data);
				break;
			case 'Message':
				$this->onMessage($data);
				break;
			case 'LivechatSession':
				$this->onClose($data);
				break;
		}

		$response->end(json_encode([
			'message' => 'OK',
		]));
	}

	private function onSessionStart(array $data): void
	{
		$this->server->push($this->getFd($data), json_encode([
			'type' => 'message',
			'text' => 'Chat iniciado',
		]));
	}

	private function onMessage(array $data): void
	{
		$message = $data['messages'][0]['msg'];

		$this->server->push($this->getFd($data), json_encode([
			'type' => 'message',
			'text' => $message,
		]));
	}

	private function getFd(array $data)
	{
		return $data['visitor']['token'];
	}

	private function onClose(array $data): void
	{
		$this->server->push($this->getFd($data), json_encode([
			'type' => 'message',
			'text' => 'Chat finalizado',
		]));

		$this->connections->delete($this->getFd($data));
	}
}

 

Finalizando a integração

Agora com todas as classes auxiliares prontas, vamos finalizar a integração.

Primeiramente precisamos ajustar o servidor para quando inicializado instanciar as classes necessárias.

Além disso precisamos também criar a tabela onde será armazenada os dados dos usuários conectados.

Estou criando uma tabela que suporta até 1024 itens e que irá armazenar o token e a sala do usuário, tendo como chave de identificação de cada usuário o seu identificador de conexão.

Agora precisamos tratar as requisições HTTP enviando para a classe responsável.

Com isso todas as requisições HTTP terão o tratamento necessário.

Agora vamos alterar os eventos do websocket.

Primeiramente alterar o evento de conexão, para realizar a criação dos dados necessários.

Agora, quando um usuário conectar ao chat precisamos realizar algumas ações.

Primeiramente vamos cria-lo como visitante no Rocket.Chat.

Em seguida vamos criar uma sala para ele.

E por último armazenas esses dados na nossa tabela de conexões.

Com tudo isso concluído, o usuário está em uma conexão válida e podendo ser atendido.

Agora vamos tratar as mensagens que ele envia.

Agora quando o usuário envia a mensagem, resgatamos os dados da sua conexão e enviamos essa mensagem ao Rocket.

Com isso temos o fluxo totalmente alterado para permitir uma conversa entre o usuário e um atendente do Rocket.

Visualizar código completo

<?php

namespace FourLinux\ChatBackend;

use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Table;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;

class ChatServer
{

    public const EVENT_START = 'start';
    public const EVENT_OPEN = 'open';
    public const EVENT_MESSAGE = 'message';
    public const EVENT_REQUEST = 'request';
    public const EVENT_CLOSE = 'close';

    private string $host;

    private int $port;

    private Table $connections;

    private Server $server;

    private Rocket $rocket;

    private RocketWebhookHandler $httpRequestHandler;

    public function __construct(
        $host = '0.0.0.0',
        $port = 9502
    ) {
        $this->host = $host;
        $this->port = $port;

        $this->rocket = new Rocket();

        $this->connections = new Table(1024);
        $this->connections->column('token', Table::TYPE_STRING, 32);
        $this->connections->column('room', Table::TYPE_STRING, 32);
        $this->connections->create();

        $this->buildServer();

        $this->httpRequestHandler = new RocketWebhookHandler($this->server, $this->connections);
    }

    private function buildServer(): void
    {
        $this->server = new Server($this->host, $this->port);

        $this->on(self::EVENT_START, 'onStart');
        $this->on(self::EVENT_OPEN, 'onOpen');
        $this->on(self::EVENT_MESSAGE, 'onMessage');
        $this->on(self::EVENT_REQUEST, 'onRequest');
        $this->on(self::EVENT_CLOSE, 'onClose');
    }

    private function on(string $event, string $method): void
    {
        $this->server->on($event, fn() => $this->$method(...func_get_args()));
    }

    public function start()
    {
        return $this->server->start();
    }

    private function onStart(Server $server): void
    {
        echo "Swoole WebSocket Server is started at ws://{$this->host}:{$this->port}", PHP_EOL;
    }

    private function onOpen(Server $server, Request $request): void
    {
        echo "connection open: {$request->fd}", PHP_EOL;

        [, $body] = $this->rocket->createVisitor($request->fd);
        $data = json_decode($body, true);
        $token = $data['visitor']['token'];

        [, $body] = $this->rocket->createRoom($token);
        $data = json_decode($body, true);
        $roomId = $data['room']['_id'];

        $this->connections->set($request->fd, [
            'token' => $token,
            'room' => $roomId,
        ]);
    }

    private function onMessage(Server $server, Frame $frame): void
    {
        $data = json_decode($frame->data, true);

        $connectionData = $this->connections->get($frame->fd);

        $this->rocket->sendMessage($connectionData['token'], $connectionData['room'], $data['text']);
    }

    private function onRequest(Request $request, Response $response): void
    {
        $this->httpRequestHandler->handle($request, $response);
    }

    private function onClose(Server $server, int $fd): void
    {
        echo "connection close: {$fd}", PHP_EOL;
    }

}

 

Testando e considerações

Podemos então testar esse fluxo e verificar se está tudo rodando como o esperto.

Podemos ver que a integração funcionou corretamente.

No inicio tínhamos um chat que envia mensagens em broadcast para todos que estavam com ele aberto.

Após os ajustes, ele realiza a integração com o Rocket, criando uma sala de atendimento no Omnichannel e permitindo que possa conversar com um atendente.

O chat não é uma aplicação completa ainda. Ele não armazena as mensagens para resgatar histórico do usuário, não permite configurar seu nome, e-mail e outros dados que possam ser necessários, e não realiza ainda muitas ações que podem ser úteis.

Mas o nosso objetivo era de mostrar como é simples fazer pequenos ajustes em um chat existente para que seja integra-lo ao Rocket.

Se você já tem um chat funcionando, pode facilmente altera-lo para permitir ter o mesmo poder desse simples chat que criei.

 

Agora que já realizamos essa integração mais simples, em um próximo post podemos mostrar como adicionar recursos mais avançados e dar mais poder ao seu chat.

Você pode visualizar o código final no nosso github: https://github.com/4linux/integrando-chat-rocket-omnichannel/tree/final.

 

Líder em Treinamento e serviços de Consultoria, Suporte e Implantação para o mundo open source. Conheça nossas soluções:

CURSOSCONSULTORIA

Anterior O que é e para que serve o famigerado "Kubernetes"?
Próxima 4Linux estará presente na Campus Party Goiás

About author

Caique Portela
Caique Portela 6 posts

Caique Portela é formado em Análise e Desenvolvimento de Sistemas na FATEC e técnico em Redes de Computadores pelo CEAP. Possui mais de 7 anos de experiência com TI, sendo pelo menos 3 anos atuando diretamente como desenvolvedor. Já trabalhou como suporte técnico e consultor de infraestrutura Linux, hoje atua como desenvolvimento e treinamento na 4Linux.

View all posts by this author →

Você pode gostar também

Desenvolvimento

Melhor Curso de HTML5 e CSS3: Por que aprender HTML5 e CSS3?

HTML é um acrônimo para Hyper Text Markup Language. E, HTML5 é uma linguagem de marcação para estruturar e exibir conteúdo para a World Wide Web (WWW). É uma versão aprimorada do padrão

Desenvolvimento

Novo Angular 4.0: o que mudará no seu dia a dia?

Neste artigo falo sobre algumas das novidades da nova versão do Angular e explico o que aconteceu com a versão 3. Confira! Compartilhe este post: Share on Twitter Share on

Treinamentos

Novo curso na trilha de Python!

Prez@dos, É com enorme satisfação que anuncio: o curso Python for Sysadmin está de cara nova. As bases do antigo curso foram mantidas, não se preocupe, mas como tudo na