# Flash My name is Barry Allen and i'm the fastest notification server alive. ![](https://cdn.pzw.io/c2520808168c8afa688c9ef591c2cc35.jpg?raw=true) ## TOC - [Instalação](#Instalação) - [Configuração](#Configuração) - [Eventos](#eventos) - [Criando novos eventos](#criando-novos-eventos) - [Adicionando eventos existentes](#adicionando-eventos-existentes) - [Enviando eventos para o Firebase](#enviando-eventos-para-o-firebase) - [Cliente](#cliente) - [Autenticação](#autenticação) - [Erros](#erros) - [Mirage](#mirage) - [Escutando eventos](#escutando-eventos) - [Conectando por um navegador](#conectando-por-um-navegador) - [Conectando por uma aplicação Node.js](#conectando-por-uma-aplicação-nodejs) - [Conectando por uma aplicação React Native](#conectando-por-uma-aplicação-react-native) # Instalação Requisitos: * [NodeJS](https://nodejs.org/en/) * [Yarn](https://yarnpkg.com/en/) * [Redis](https://https://redis.io/) O Yarn foi escolhido por conta do avançado sistema de cache, entratanto, pode ser utilizado com o npm padrão. ``` bash yarn ``` Após a instação das dependências, executar o comando para rodar o servidor local. ``` bash yarn start ``` ## Uso .Flash/ ├── configs ├── examples ├── interfaces ├── middlewares └── tests A seguinte estrutura é descrita: * configs/ arquivos em JS que fazem o setup de dependências chaves para o correto funcionamento da aplicação. * examples/ exemplos relacionados à como se conectar no servidor de notificações via navegador, aplicação node ou *mobile*, servindo também para utlizar como clientes de teste. * interfaces/ abstrações de integrações com serviços externos como por exemplo: Firebase e Huggy. * middlewares/ funções para intermediar rotas. * tests/ arquivos em JS dos testes automatizados. ## Links adicionais * <a href="https://socket.io/" target="_blank">Documentação do Socket.io</a> * <a href="https://github.com/socketio/engine.io" target="_blank">Documentação do Engine.io</a> * <a href="https://github.com/primus/primus" target="_blank">Documentação do Primus</a> * <a href="https://github.com/cayasso/primus-emitter" target="_blank">Documentação do PrimusEmitter</a> * <a href="https://github.com/cayasso/primus-rooms" target="_blank">Documentação do PrimusRooms</a> * <a href="https://github.com/primus/mirage" target="_blank">Documentação do Mirage</a> # Configuração Realizar uma cópia do .env.example. ``` bash cp .env.example .env ``` ## Variáveis de ambiente | Variável | Descrição | |:----------:|:-------------:| | APP_ENV | Ambiente da aplicação, podendo ser *development*, *testing*, *staging* ou *production*. | | APP_SENTRY_DSN | Callback do Sentry para o envio de exceções/logs. | | SERVER_PROTOCOL | Protocolo utilizado pelo servidor de notificações, podendo ser http ou https. | | SERVER_HOST | Endereço do servidor de notificações. | | SERVER_PORT | Porta do servidor de notificações. | | FORCE_HTTPS | Forçar protocolo https. | | PRIMUS_TRANSFORMER | Engine de socket que será utilizada pelo Primus (Default: engine.io). Vale lembrar que as dependências de cada engine precisam estar instaladas para o correto funcionamento. | | PRIMUS_SESSION_ID_TIMEOUT | Tempo máximo que o Primus deve levar para validar ou gerar um sessão. | | REDIS_HOST | Endereço do banco Redis. | | REDIS_PORT | Porta do banco Redis. | | REDIS_CHANNEL | Canal em comum ao Painel e o Socket para uso do protocolo PUB/SUB. | | AUTH_ROOT_CLIENT_HASH | Hash de autenticação do Socket pai. | | FIREBASE_ENDPOINT | Callback do Firebase para o envio de notificações. | | HUGGY_API_ENDPOINT | URL da API Huggy. | **Nota: Com o ambiente da aplicação (APP_ENV) definido como *development*, serviços externos como Sentry e Firebase serão desabilitados.** # Eventos O Painel da Huggy já dispara uma grande quantidade de eventos, com isso, vários eventos já foram configurados para serem enviados ao Flash, são eles: | Nome do evento | Grupo | Label | Broadcast | Firebase | |:------------------:|:---------:|:---------------:|:-----------:|:----------:| | Chat criado | CHAT | company:<company_id>:chat:created | :heavy_check_mark: | :x: | | Chat colocado na fila | CHAT | company:<company_id>:chat:put-in-queue | :heavy_check_mark: | :x: | | Agente entrou no chat | CHAT | company:<company_id>:chat:agent-joined | :x: | :x: | | Chat finalizado | CHAT | company:<company_id>:chat:finished | :x: | :x: | | Chat lido | CHAT | company:<company_id>:chat:read | :x: | :x: | | Agente saiu do chat | CHAT | company:<company_id>:chat:agent-left | :x: | :x: | | Agente adicionado | CHAT | company:<company_id>:chat:added-agent | :x: | :x: | | Chamada recebida | CHAMADA | company:<company_id>:call:incoming | :heavy_check_mark: | :x: | | Chamada encerrada | CHAMADA | company:<company_id>:call:hangup | :x: | :x: | | Chamada transferida | CHAMADA | company:<company_id>:call:transferred | :x: | :x: | | Nova mensagem | MENSAGEM | company:<company_id>:message:new | :x: | :heavy_check_mark: | | Novo evento interno | MENSAGEM | company:<company_id>:message:event | :heavy_check_mark: | :heavy_check_mark: | | Nova mensagem interna | MENSAGEM | company:<company_id>:message:internal | :x: | :heavy_check_mark: | | Status de agente | AGENTE | company:<company_id>:agent:status | :heavy_check_mark: | :x: | | Novo departamento | CONFIGURAÇÃO | company:<company_id>:config:new-department | :heavy_check_mark: | :x: | | Departamento editado | CONFIGURAÇÃO | company:<company_id>:config:updated-department | :heavy_check_mark: | :x: | | Nova tabulação | CONFIGURAÇÃO | company:<company_id>:config:new-tabulation | :heavy_check_mark: | :x: | | Tabulação editada | CONFIGURAÇÃO | company:<company_id>:config:updated-tabulation | :heavy_check_mark: | :x: | | Tabulação removida | CONFIGURAÇÃO | company:<company_id>:config:removed-tabulation | :heavy_check_mark: | :x: | | Novo atalho | CONFIGURAÇÃO | company:<company_id>:config:new-shortcut | :heavy_check_mark: | :x: | | Atalho editado | CONFIGURAÇÃO | company:<company_id>:config:updated-shortcut | :heavy_check_mark: | :x: | | Atalho removido | CONFIGURAÇÃO | company:<company_id>:config:removed-shortcut | :heavy_check_mark: | :x: | Mas e aí? Se surgir uma demanda de novos eventos, o que devo fazer? Basta seguir com os tópicos abaixo para a [criação de um novo evento](#criando-novos-eventos) ou [adição de um evento já existente](#adicionando-eventos-existentes). **Nota: Para mais informações sobre os *payloads* de cada evento ou quais destes são do tipo *broadcast* ou não, acesse a [documentação mais técnica](https://huggydigital.atlassian.net/wiki/spaces/PROD/pages/133169153/Novo+Socket).** ## Criando novos eventos Os eventos do Painel, ficam no diretório (Tendo como referência pasta raiz do Painel da Huggy): ``` ./Src/Hooks/Events ``` ### Exemplo Suponha que você precise criar um evento que mapeie o momento em que um novo departamento foi registrado. Assim, você deve criar uma nova classe PHP dentro deste diretório, que herdará de **[HookEvent](#herdando-a-classe-hookevent)** e implementará as interfaces **[Event](#implementando-a-interface-event)** e **[SocketEvent](#implementando-a-interface-socketevent)**. O nome do evento deverá estar em inglês e no passado, como segue no exemplo a seguir. ``` php <?php namespace App\Hooks\Events; use App\Hooks\Contracts\HookEvent; use App\Hooks\Events\Interfaces\Event; use App\Hooks\Events\Interfaces\SocketEvent; use App\Model\Department; final class CreatedDepartmentEvent extends HookEvent implements Event, SocketEvent { const NAME = 'APP_LABEL_EVENT_CREATED_DEPARTMENT'; const DESCRIPTION = 'APP_LABEL_EVENT_CREATED_DEPARTMENT_DESCRIPTION'; const KEY = 'createdDepartment'; /** * @var Department */ private $department; public function __construct(Department $department) { $this->department = $department; } public function getDepartment(): Department { return $this->department; } public function getCompanyID() { return $this->getDepartment()->getCompanyID(); } public function getWebhookData() { return []; } public static function getName(): string { return self::NAME; } public static function getDescription(): string { return self::DESCRIPTION; } public static function getKey(): string { return self::KEY; } public static function getData(): array { return [ 'name' => self::getName(), 'description' => self::getDescription(), 'key' => self::getKey() ]; } public function getSocketData(): array { $data['payload'] = $this->getDepartment()->jsonSerialize(); $data['companyId'] = $this->getCompanyID(); $data['wildcards'] = "company:{$this->getCompanyID()}:broadcast"; $data['label'] = "company:{$this->getCompanyID()}:config:new-department"; return $data; } } ``` #### Herdando a classe HookEvent Quando se herda a classe **HookEvent**, obrigatoriamente, você precisará sobrescrever alguns métodos, que são: ``` php public function getCompanyID() { return $this->getDepartment()->getCompanyID(); } ``` Logo, você precisará trabalhar com a sua entidade para ela retornar o código da empresa a qual ela pertence através do método ```getCompanyId()```. Neste exemplo, como a entidade trata-se do Departamento, o código da empresa foi acessado pelo método do objeto **Departamento** que retorna o mesmo. ``` php public function getWebhookData() { return []; } ``` Já para o outro método ```getWebhookData()```, se ele for utilizado apenas pelo **Flash**, você pode retornar um array vazio ```[]```. Entretanto, se ele for utilizado pelo Webhook do Painel da Huggy, você precisará definir o conjunto de dados que será enviado no momento do disparo desse evento. #### Implementando a interface Event Já para interface **Event**, obrigatoriamente, você precisará implementar algumas constantes e métodos, que são: ###### Constantes ``` php const NAME = 'APP_LABEL_EVENT_CREATED_DEPARTMENT'; const DESCRIPTION = 'APP_LABEL_EVENT_CREATED_DEPARTMENT_DESCRIPTION'; const KEY = 'createdDepartment'; ``` Assim, é necessário definir ```NAME```, ```DESCRIPTION``` e ```KEY``` seguindo o padrão de acordo com o nome do evento. Também, você precisará criar seus métodos de acesso (*Getters*). ###### Métodos ``` php public static function getName(): string { return self::NAME; } public static function getDescription(): string { return self::DESCRIPTION; } public static function getKey(): string { return self::KEY; } ``` Sendo que um desses *getters*, o ```getData()``` deve reunir em um *array* todas as constantes já definidas. ``` php public static function getData(): array { return [ 'name' => self::getName(), 'description' => self::getDescription(), 'key' => self::getKey() ]; } ``` #### Implementando a interface SocketEvent Por fim, para a interface **SocketEvent**, obrigatoriamente, você precisará implementar o método ```getSocketData()```, pois ele é essencial para o **Flash** processar o evento corretamente. Se o **Flash** encontrar alguma anormalidade no *payload* do evento, o mesmo será descartado. Observe o exemplo a seguir: ``` php public function getSocketData(): array { $data['payload'] = $this->getDepartment()->jsonSerialize(); $data['companyId'] = $this->getCompanyID(); $data['wildcards'] = "company:{$this->getCompanyID()}:broadcast"; $data['label'] = "company:{$this->getCompanyID()}:config:new-department"; return $data; } ``` Por padrão, para o seu conjunto de dados não ser descartado, o mesmo deverá ter pelo menos os seguintes campos: ```payload```, ```wildcards``` e ```label```. ##### Payload Este campo conterá o conjunto de dados referente ao evento. ##### Wildcards Este campo conterá a(s) *room(s)* que receberão esse evento, uma *room* pode ser do tipo ***broadcast*** ou **apenas para agentes interessados**. ###### Estrutura ```company:<company_id>:brodcast``` ou ```company:<company_id>:agent:<agent_id>``` ###### Broadcast ```company:1:brodcast``` ###### Agentes interessados ```company:1:agent:99 company:1:agent:66 company:1:agent:104 ...``` **Nota: Para gerar rooms apenas para os agentes interessados, vá para [essa seção](#gerando-wildcards-para-agentes-interessados).** ##### Label Este campo conterá o tipo/tópico/nome do evento que será utilizado pelo [Cliente](#cliente) no momento de escutá-los. ###### Estrutura ```company:<company_id>:<grupo_do_evento>:<nome_do_evento>``` ###### Exemplo Ainda com o evento de criação do *Departamento*. Suponha que essa entidade pertença ao módulo (Nome do grupo de eventos) de configurações. Logo, seu label poderia ser estruturado da seguinte forma: ```company:<company_id>>:config:new-department``` E seu uso para uma empresa de código igual a 1 seria algo como: ```company:1>:config:new-department``` #### Gerando Wildcards para agentes interessados Para facilitar a nossa vida, já existe uma biblioteca do Primus no projeto do Painel da Huggy, que nos auxilia na geração de *wildcards*, a mesma está localizada em: ``` ./Src/Libraries/Primus ``` Seu uso é muito simples, sendo necessário passar como parâmetro apenas o código da empresa e um *array* dos agentes (Objetos) que serão interessados. Podemos ver sua assinatura a seguir: ``` php Primus::buildWildcards(int $companyID, array $agentes): string; ``` **Nota: Não se esqueça de importar a biblioteca do Primus quando for utilizá-la.** #### Registrando o evento no Provider Evento implementado?! Se sim, agora você já pode registrá-lo no **Provider** do **Flash**, que se encontra em: ``` ./Src/Hooks/Providers ``` Dentro deste diretório existe um arquivo chamado ``Flash.php``, que contém toda a configuração dos eventos que serão disparados para o **Flash**, e também, outras regras de negócio. Mas o que nos importa agora, é registrar o novo evento criado, que nesse caso, é o **CreatedDepartmentEvent**. Para isso, existe um método neste **Provider** chamado ```getEventInterests()```. Este método retorna um array que contém todos os eventos que nós temos interesse de enviar para o **Flash**. Assim, basta inserí-lo no final desse array, como segue: ``` php public static function getEventInterests() { return [ /* ...Outros eventos... */ CreatedDepartmentEvent::class, ]; } ``` **Nota: Não se esqueça de importar a classe do evento no Provider quando for utilizá-la.** #### Disparando o evento Com o evento registrado, já podemos dispará-lo no local que faça mais sentido para o ecossistema da aplicação. Por exemplo, se o evento que registramos mapeia o momento em que um novo departamento é criado, nada mais justo que adicioná-lo dentro do *repository* de *Departamento* logo após a operação que salva a nova informação no banco de dados. E para dispararmos o evento, usamos a seguinte sintaxe: ``` php /* ...Save do banco e outras operações... */ Hook::dispatch(new CreatedDepartmentEvent($department)); ``` **Nota: Não se esqueça de importar a classe do evento quando for utilizá-la.** ## Adicionando eventos existentes Se o evento já existe no Painel, o caminho é mais simples, basta você implementar a *interface* **SocketEvent** neste evento, e criar o método ```getSocketData()```. Então, pule para [aqui](#implementando-a-interface-socketevent). ## Enviando eventos para o Firebase Como é de nosso conhecimento, o aplicativo *mobile* não fica conectado ao **Flash** o tempo todo, logo, o **Flash** envia esses eventos também para o **Firebase**. Porém, se torna inviável disparar todos os tipos de eventos para lá, já que existe um custo para cada execução e não são todos os eventos que precisarão ser notificados para o usuário. Assim, existe um método no **Provider** do **Flash** chamado ```getRegisteredsEventsFirebase()```. Este método retorna um array que contém todos os eventos que nós temos interesse de enviar para o **Firebase**. Portanto, basta inserí-lo no final desse array, como segue: ``` php public static function getRegisteredsEventsFirebase() { return [ /* ...Outros eventos... */ CreatedDepartmentEvent::class, ]; } ``` **Nota: Não se esqueça de importar a classe do evento no Provider quando for utilizá-la.** # Cliente Para clientes se conectarem no **Flash** é muito simples. Se o cliente estiver realizando a conexão através de um navegador, será necessário expor a biblioteca primus.js em um servidor estático ou CDN. Não iremos nos aprofundarmos nesse tópico aqui, já que a própria documentação do **Primus** tem uma seção que trata deste ponto, [clique aqui para acessar](https://github.com/primus/primus#client-library). ## Autenticação A autenticação do **Flash** utiliza um padrão de mercado, onde você precisará passar o ***token*** de acesso no momento de abrir a conexão com o servidor de notificações. Para isso, basta trafegar o ***token*** recebido da API Huggy após consultar as informações do recurso *payload*. Este ***token*** se encontra dentro da propriedade ``socket``, cujo nome é ```agentToken```. Como esse ***token*** em mãos, ele pode ser passado da seguinte forma: ###### Demonstração usando Node.js ```js /* Request do agentToken... */ const Primus = require('primus'); const Emitter = require('primus-emitter'); const Socket = Primus.createSocket({ transformer: 'websockets', plugin: { 'emitter': Emitter } }); const socket = new Socket('ws://localhost:3333'); socket.on('outgoing::url', function connectionURL(url) { url.query = `token=${agentToken}`; }); ``` **Nota: Por convenção, com o Primus nós devemos usar um evento privado chamado `outgoing::url` para inserir o *token* no *query string* da URL de conexão.** ### Erros De maneira semelhante à seção anterior. Para sabermos se houve algum problema de conexão com o servidor de notificações, devemos escutar os eventos privados ```outgoing::open``` e ```unexpected-response``` da seguinte forma: ###### Demonstração usando Nodej.js ```js /* Request do agentToken, inicialização do Primus, autenticação... */ socket.on('outgoing::open', function () { socket.socket.on('unexpected-response', function (req, res) { console.error(res.statusCode); console.error(res.headers['www-authenticate']); req.abort(); //Sua função que renderiza a mensagem de erro para o usuário }); }); ``` ### Mirage O Mirage é um dos *plugins* do Primus utilizado pelo **Flash** que ajuda no controle de sessões. Quando o cliente consegue se conectar, é retornado uma sessão através do evento `mirage`. Esta sessão deve ser persistida e utilizada nas próximas conexões. Caso seja sua primeira conexão, pode ser passado `null` ou `''`. ###### Demonstração usando Nodej.js ```js const Primus = require('primus'); const Emitter = require('primus-emitter'); const Socket = Primus.createSocket({ transformer: 'websockets', plugin: { 'emitter': Emitter }, mirage: //Deve ser sua função que busca a sessão de algum lugar, se não existir, retorne null. }); const socket = new Socket('ws://localhost:3333'); socket.on('mirage', function (data) { //Grave sua sessão em algum lugar (Ex: redis, temp, file, localStorage...) }); ``` ## Escutando eventos Como foi visto na seção de [Eventos](#eventos), nós já temos alguns eventos para serem escutados, para assim, tornar nossas aplicações mais interativas. Portanto, basta escutar os eventos de seu interesse e realizar as operações necessárias dentro da sua aplicação. ```js /* Request do agentToken, inicialização do Primus, autenticação... */ socket.on('company:<company_id>:chat:created', function (data) { //Sua função para renderizar o chat, notificar o usuário, etc... }); socket.on('company:<company_id>:message:new', function (data) { //Sua função para notificar o usuário, etc... }); socket.on('company:<company_id>:agent:status', function (data) { //Sua função para atualizar o status do agente, etc... }); /* Outros eventos... */ ``` ## Conectando por um navegador Se você seguiu o passo a passo de [como expor o primus.js](https://github.com/primus/primus#client-library), segue uma aplicação simples que se conecta ao **Flash**, escuta os eventos de autenticação, sessão e de chat criado. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script> <script src="../primus.js"></script> <script> $(document).ready(function () { /* Request à API Huggy que retornou o agentToken... */ var primus = Primus.connect('http://localhost:3333', { mirage: localStorage.getItem('sessionid') }); primus.on('outgoing::url', function connectionURL(url) { url.query = 'token=' + agentToken; }); primus.on('outgoing::open', function () { primus.socket.on('unexpected-response', function (req, res) { console.error(res.statusCode); console.error(res.headers['www-authenticate']); req.abort(); alert('Error message'); }); }); primus.on('mirage', function (id) { localStorage.setItem('sessionid', id) }); primus.on('company:<company_id>:chat:created', function (data) { console.log('CHAT'); console.log(data); }); }); </script> </head> <body> </body> </html> ``` ## Conectando por uma aplicação Node.js Aqui temos um exemplo semelhante ao anterior, porém, utilizando Node.js. ```js const Primus = require('primus'); const Emitter = require('primus-emitter'); /* Request à API Huggy que retornou o agentToken... */ const Socket = Primus.createSocket({ transformer: 'websockets', plugin: { 'emitter': Emitter }, mirage: //Deve ser sua função que busca a sessão de algum lugar, se não existir, retorne null. }); const socket = new Socket('ws://localhost:3333'); socket.on('outgoing::url', function connectionURL(url) { url.query = `token=${agentToken}`; }); socket.on('outgoing::open', function () { socket.socket.on('unexpected-response', function (req, res) { console.error(res.statusCode); console.error(res.headers['www-authenticate']); req.abort(); console.log('error', 'authorization failed: ' + res.statusCode); }); }); socket.on('company:<company_id>:chat:created', function (data) { console.log('CHAT', data); }); socket.on('mirage', function (data) { //Grave sua sessão em algum lugar (Ex: redis, temp, file, localStorage...) }); ``` **Nota: Vale ressaltar que você precisa instalar as dependências com o yarn ou npm.** ## Conectando por uma aplicação React Native Utilizando o React Native, o processo se torna um pouco mais confuso pelo fato dele não ter algumas dependências nativas. Diante disto, é necessário gerar o primus.js localmente para importá-lo dentro da aplicação. Mas não se preocupe, alguém já passou por isso e criou um [exemplo bem completo](https://github.com/actionhero/actionhero-react-native-example).