# Flash
My name is Barry Allen and i'm the fastest notification server alive.

## 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).