NeoLogo

Dígitro Neo Interact CTI Computer Telephony Integration

Assinando eventos



subscribe

Comando utilizado para criar ou renovar assinaturas. Para receber notificação de eventos que ocorrem no Neo Interact é necessário assiná-los. A assinatura é feita pelo comando subscribe da API REST do Neo Interact CTI.

Para criar uma nova assinatura de eventos, o campo subscribe deverá ser enviado ao Neo Interact sem o id. Se o id estiver presente, o comando será considerado como uma renovação de assinatura. A assinatura de eventos também deve especificar qual canal será utilizado para a recepção dos eventos, sendo mandatório que pelo menos um deles seja especificado. Há dois canais de assinatura possíveis:

Ambas técnicas foram descritas anteriormente. Quando canais SSE forem utilizados como canal de assinatura, eles devem ser abertos ANTES da solicitação da assinatura, pois é necessário referenciar o canal SSE por onde os eventos irão trafegar.

Toda assinatura possui um tempo de expiração, dado pelo campo expires informado na resposta do comando subscribe. Para que a assinatura continue válida, é necessário que sua renovação ocorra antes deste tempo. Na renovação da assinatura, os canais de notificação e filtro de eventos podem ser alterados.


Exemplo: Cliente CTI assina eventos em um canal SSE (channel_id) previamente conectado

Corpo da requisicao ao criar assinatura (sem filtro de eventos).

{
  "subscription": {
    "channel_id": 1,
  }
}


Corpo da resposta ao criar assinatura. O campo id representa o identificador da assinatura. Ele será necessário para revalidar e/ou cancelar a assinatura. A assinatura deve ser renovada antes do tempo de expiração (em segundos) indicado.

{
  "response": {
    "command": "subscribe",
    "id": "f2ba15e6-de7c-e291-b352-fa3d28d15de4",
    "status": "ok",
    "expires": 900,
  }
}



get_subscriptions

As assinaturas ativas podem ser consultadas pelo comando get_subscriptions, informando-se o id da assinatura ou o canal associado.

Exemplo: Cliente cti verifica assinatura no servidor

Corpo da resposta ao verificar assinatura

"response": {
  "resource": "agent",
  "command": "get_subscriptions",
  "status": "ok",
  "subscriptions": [
    {
      "channel_id": 9,
      "expires": 900,
      "agents": [
        "agent1.neocti@digitro.com",
        "agent2.neocti@digitro.com",
      ],
      "events": [
        "all"
      ],
      "services": [],
      "media_types": [
        "chat"
      ],
      "items": [
        "agents_list",
        "classification_list"
      ],
      "id": "91ca15e6-cd7a-4281-b322-fa3d18d15d35"
    }
  ]
}



unsubscribe

Uma assinatura poderá ser cancelada ao se enviar um comando de unsubscribe informando o código identificador da assinatura (id).


Exemplo: Cliente cti encerra assinatura de eventos

Corpo da requisicao de encerrar assinatura

{
  "id": "f2ba15e6-de7c-e291-b352-fa3d28d15de4"
}

Corpo da resposta ao encerrar assinatura

{
  "response": {
    "command": "unsubscribe",
    "id": "f2ba15e6-de7c-e291-b352-fa3d28d15de4",
    "status": "ok"
  }
}



Diagrama de assinatura de eventos

 CLIENT                                          NEO-CTI
   |                                                |
   |                                                |
   | Estabelecendo conexao SSE                      |
   |----------------------------------------------->| GET /sse/open_channel
   |<-----------------------------------------------| EVENT on_open_channel { channel_id: 1 }
   |                                                |
   |                                                |
   | Com a conexao SSE estabelecida, pode-se        |
   | assinar eventos                                |
   |----------------------------------------------->| POST /subscription/subscribe
   |<-----------------------------------------------| 200 OK { subscription_id }
   |                                                |
   |                                                |
   | Com a assinatura realizada, eventos são        |
   | transmitidos                                   |
   |<-----------------------------------------------| { event: on_login, ...}
   |                                                |
   |                                                |
   | Renovando a assinatura antes do tempo de       |
   | expiração                                      |
   |----------------------------------------------->| POST /subscription/subscribe (subscription_id)
   |<-----------------------------------------------| 200 OK
   |                                                |
   |                                                |
   | Verificando o estado da assinatura no servidor |
   |----------------------------------------------->| GET /subscription/get_subscriptions
   |<-----------------------------------------------| 200 OK
   |                                                |
   |                                                |
   | Encerrando a assinatura de eventos             |
   |----------------------------------------------->| POST /subscription/unsubscribe
   |<-----------------------------------------------| 200 OK



Assinando todos os eventos

O código exemplo abaixo:

  1. Estabelece um canal SSE
  2. Assina todos os eventos disponíveis (não aplica filtros de eventos)
  3. Periodicamente renova a assinatura dos eventos


const EventSource = require('eventsource')
const fetch = require('node-fetch')

function openSseChannel(ctiUrl, apiKey) {
    return (new Promise((resolve, reject) => {
        const url = `${ctiUrl}/sse/open_channel`
        sseChannel = new EventSource(url, { headers: { 'X-API-KEY': apiKey } })
        sseChannel.ctiUrl = ctiUrl
        sseChannel.apiKey = apiKey

        sseChannel.on('open', (msg) => {
            console.log(`open`, msg)
        })
        sseChannel.on('error', (err) => {
            console.log(`error`, err)
        })
        sseChannel.on('message', (msg) => {
            const data = JSON.parse(msg.data)
            console.log('heartbeating - data:', JSON.stringify(data, null, 2))
        })

        sseChannel.on('on_open_channel', (msg) => {
            const data = JSON.parse(msg.data)
            console.log('on_open_channel',  JSON.stringify(data, null, 2))
            sseChannel.id = Number(data.channel_id)
            resolve(sseChannel)
        })

        const events = [
            'on_login',
            'on_logout',
            'on_state_change',
            'on_config_change',
            'on_call_receive',
            'on_call_accept',
            'on_call_release',
            'on_call_end',
            'on_call_associated_data',
            'on_call_generate',
            'on_call_hold',
            'on_call_retrieve',
            'on_call_consultation',
            'on_call_transfer',
            'on_call_conference',
            'on_chat_message',
            'on_typing',
            'on_call_record',
            'on_agent_status',
        ]

        events.forEach((event) => {
            sseChannel.on(event, (msg) => {
                const data = JSON.parse(msg.data)
                console.log(event,  JSON.stringify(data, null, 2))
            })
        })
    }))
}

async function subscribeCtiEvents({ ctiUrl, apiKey, sseChannelId, webhook, subscriptionId, filters }) {
    let subscription = {}
    if (subscriptionId) {
        subscription.id = subscriptionId
    }
    if (filters) {
        subscription = { ...subscription, ...filters }
    }
    if (webhook) {
        subscription.webhook = webhook
    } else {
        subscription.channel_id = sseChannelId
    }

    const headers = {
        'X-API-KEY': apiKey,
        'Content-Type': 'application/json'
    }
    const response = await fetch(`${ctiUrl}/subscription/subscribe`, { method: 'POST', headers, body: JSON.stringify({ subscription }) })
    if (response.status === 200) {
        const body = await response.json()
        return body
    }
    return
}

async function main(ctiUrl, apiKey) {
    const sseChannel = await openSseChannel(ctiUrl, apiKey)
    console.log(`SSE channel id: ${sseChannel.id} opened`)

    const filters = {} // A ausência de filtros, implica na assinatura de todos eventos daquelas categorias
    const sseChannelId = sseChannel.id
    const body = await subscribeCtiEvents({ ctiUrl, apiKey, sseChannelId, filters })
    let subscriptionId = body.response.id
    let expires = body.response.expires
    console.log(`Subscribing events - Subscription id: ${subscriptionId}`)
    setInterval(async () => {
        const body = await subscribeCtiEvents({ ctiUrl, apiKey, sseChannelId, subscriptionId, filters })
        subscriptionId = body.response.id
        expires = body.response.expires
        console.log(`Subscription id: ${subscriptionId} renewed`)
    }, (expires - 30) * 1000) // Renova 30 segundos antes de expirar a validade da assinatura
}

const apiKey = '1f9cdebf-387c-4dc5-a0cd-ac873132ba90'  // Alterar para sua API-KEY
const domain = 'client.saas'                           // Alterar para seu domínio
const ctiUrl = `https://${domain}.digitro.cloud/neo/cti/v1.0.0/`

main(ctiUrl, apiKey)


Variação do código exemplo acima com a passagem da apiKey via query string:

(...)
const querystring = require('querystring')

function openSseChannel(ctiUrl, apiKey) {
    return (new Promise((resolve, reject) => {
        const url = `${ctiUrl}/sse/open_channel?${querystring.encode({ apiKey })}`
        sseChannel = new EventSource(url)
(...)

ATENÇÃO:

  1. Em caso de queda do canal SSE, a aplicação cliente deverá reconectar o canal SSE (a classe EventSource implementa isso automaticamente) e uma nova assinatura de eventos deverá ser refeita. Quando o servidor percebe a queda do canal SSE, as assinaturas de eventos associadas ao canal são automaticamente excluídas.
  2. Nos casos onde o webhook for utilizado como canal para a notificação de eventos, o servidor não possui meios para inferir que a aplicação cliente possa ter morrido. Portanto as assinaturas feitas por este canal ficam válidas até sua expiração. Caso a aplicação cliente se ative novamente e faça um novo subscribe e ainda existir um assinatura válida para este canal, este subscribe será tratado como uma renovação de assinatura e seu id será enviado na resposta, como se fosse criado uma nova assinatura.



Filtrando eventos

Muito embora o tratamento de eventos possa ser filtrado na própria aplicação do cliente, opcionalmente pode ser desejável que nem todos os eventos do Contact Center sejam reportados num mesmo canal, seja por restrições de tráfego, por restrições de processamento da aplicação ou por restrição de escopo, por exemplo.

O comando subscribe permite que filtros sejam aplicados por assinatura de eventos. Esses filtros são configurados nos dados passados no comando pelos campos:

Esses campos são arrays de strings nas quais são especificados os elementos de cada filtro que se deseja monitorar, seguindo as seguintes regras:

ATENÇÃO: A criação de múltiplos filtros pode levar a resultados inesperados, por exemplo a criação de um filtro de agentes com um agente específico e outro filtro de eventos com evento específico poderão ter suas ações somadas, ou seja, o critério de encaminhamento de eventos deverá atender ambos os filtros, no nosso exemplo, somente eventos específicos daqueles agentes específicos serão enviados.


Exemplo:

{
  "subscription": {
    "id": 1,
    "channel_id": 1,
    "expires": 3600,
    "agents": [ "agente1.neocti@digitro.cloud" ],
    "events": [ "on_login", "on_logout", "on_call_receive" ],
    "services": [ "*" ],
    "media_types": [],
    // "items": [ "services_list", "agents_list", "pause_type_list", "classification_list" ],
  }
}

No exemplo acima são definidos 5 filtros com as seguintes regras:

  1. Eventos de agentes só serão enviados se forem de "agente1.neocti@digitro.cloud"
  2. Somente os eventos "on_login", "on_logout", "on_call_receive" serão encaminhados.
  3. Eventos de chamadas de quaisquer serviços serão encaminhados
  4. Eventos de chamadas com qualquer tipo de mídia serão bloqueados.
  5. Todos os eventos de alteração de itens da lista serão encaminhados

Atenção:

Para adicionar filtros ao código acima, basta acrescentar, por exemplo:

(...)

async function main(ctiUrl, apiKey, expires) {
    const sseChannel = await openSseChannel(ctiUrl, apiKey)
    console.log(`SSE channel id: ${sseChannel.id} opened`)

    const filters = {
        agents: ['agente1.neocti@digitro.com', 'agente2.neocti@digitro.com']
        items: ['agents_list', 'classification_list'],
        services:['*'],
        media_types: ['chat'],
        // events: ['all'], - A ausência do filtro implica na assinatura de todos eventos
    }

    const sseChannelId = sseChannel.id
    const body = await subscribeCtiEvents({ ctiUrl, apiKey, sseChannelId, filters })
    let subscriptionId = body.response.id
    let expires = body.response.expires
    console.log(`Subscribing events - Subscription id: ${subscriptionId}`)
    setInterval(async () => {
        const body = await subscribeCtiEvents({ ctiUrl, apiKey, sseChannelId, subscriptionId, filters })
        subscriptionId = body.response.id
        expires = body.response.expires
        console.log(`Subscription id: ${subscriptionId} renewed`)
    }, (expires - 30) * 1000) // Renova 30 segundos antes de expirar a validade da assinatura
}

(...)