---
# System prepended metadata

title: Flash

---

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