https://a.storyblok.com/f/270183/312352/07186364b7/building-a-javascript-hotline-with-opentok-and-node-js.png

Criação de uma linha direta em JavaScript com OpenTok e Node.js

Publicado em April 27, 2021

Tempo de leitura: 11 minutos

Antigamente, quando as pessoas tinham dúvidas sobre JavaScript, elas acessavam o IRC (Internet Relay Chat) para encontrar alguém que pudesse respondê-las. No entanto, o IRC é uma tecnologia muito antiga, em termos da Internet. Muitos desenvolvedores de JavaScript hoje em dia talvez nem tenham um cliente de IRC, ou talvez nunca tenham usado um. A maioria, por outro lado, está bem familiarizada com o chat por Video.

O OpenTok já facilita o início de um bate-papo por vídeo simples entre dois participantes. Com um pouco de lógica de fila adicionada, você pode ter uma linha direta para dúvidas sobre JavaScript — ou qualquer outro assunto — em um piscar de olhos.

Para tornar sua linha direta ainda mais acessível, você pode desenvolver o projeto em Glitch. Isso significa menos trabalho de configuração para você. Além disso, torna sua linha direta ainda mais útil, ao disponibilizar o projeto completo para que outras pessoas possam adaptá-lo para suas próprias linhas diretas.

Introdução ao Glitch

Se você quiser ir direto para um projeto que já funciona, pode fazer um remix do projeto “JavaScript hotline” no Glitch imediatamente. Caso contrário, em apenas algumas etapas, você pode programar sua própria linha direta do zero. Para começar, crie um novo projeto no Glitch, escolhendo o hello-express modelo.

Para oferecer chat por vídeo com o OpenTok, opentok esse é, na verdade, o único pacote adicional de que você precisa. No entanto, adicionar a opção de receber uma mensagem de texto quando alguém precisar de uma resposta a uma pergunta tornará a linha de atendimento mais capaz de lidar com variações no volume de usuários. Para oferecer esse suporte, você também pode instalar body-parser para receber dados inseridos em formulários e nexmo para enviar suas mensagens de texto:

pnpm install opentok body-parser nexmo -s

Você pode adaptar o exemplo server.js, index.htmle client.js arquivos que já estão no seu projeto do Glitch. Isso significa que sua configuração está praticamente pronta.

Definição de variáveis de ambiente

Seu .env arquivo ficará mais ou menos assim:

OPENTOK_API_KEY="12345678" OPENTOK_SECRET="12a3b4c567d89e0f1234567890ab12345678c901" NEXMO_API_KEY="12ab3456" NEXMO_API_SECRET="123AbcdefghIJklM" FROM_PHONE="441234567890"

Para atribuir valores reais a essas variáveis, você precisará de contas de desenvolvedor tanto no OpenTok quanto no Nexmo. Você também precisará de um projeto no OpenTok e de um número virtual do Nexmo.

No seu painel da sua conta OpenTok, crie um novo projeto para sua linha direta com o tipo de projeto “OpenTok API”. Depois de dar um nome a ele e selecionar um codec de vídeo (o VP8 deve servir), você verá a chave e o segredo da API. Você pode colá-los em OPENTOK_API_KEY e OPENTOK_SECRET, respectivamente.

Suas credenciais do Nexmo, que você pode colar em NEXMO_API_KEY e NEXMO_API_SECRET, devem estar visíveis na página “Introdução” do seu Painel do Nexmo. Você pode usar qualquer número de telefone da seção “[Seus Numbers](${CUSTOMER_DASHBOARD_URL}/your-numbers)” sem precisar de configuração para FROM_PHONE, já que você enviará mensagens de texto a partir desse número, mas não receberá mensagens nem chamadas.

Configurando o servidor

Seu servidor já inclui uma inicialização, uma rota de visualização para a raiz do aplicativo e um ouvinte que inicia o servidor. Você pode modificar um pouco a seção de inicialização para usar também o body-parser middleware:

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(express.static('public'));
app.use(bodyParser.json());

Abaixo desse bloco, você pode adicionar novos objetos OpenTok e Nexmo inicializados com os valores de .enve duas matrizes vazias. waiting é para os IDs de sessão dos chats que aguardam que um assistente responda a uma pergunta, e helpers contém os números de telefone das pessoas que se ofereceram para ajudar durante os períodos de inatividade, quando ninguém tinha uma pergunta.

const OpenTok = require('opentok');
const opentok = new OpenTok(process.env.OPENTOK_API_KEY, process.env.OPENTOK_SECRET);

const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: process.env.NEXMO_API_KEY,
  apiSecret: process.env.NEXMO_API_SECRET
});

var waiting = [];
var helpers = [];

No restante do arquivo, entre a rota padrão e o listener, você pode declarar as outras rotas. Abaixo delas, você precisará de uma função para enviar uma mensagem de texto a alguém que se ofereceu para responder às perguntas:

app.get('/', function(request, response) {
  response.sendFile(__dirname + '/views/index.html');
});

app.get('/ask', function(request, response) {});

app.get('/answer', function(request, response) {});

app.get('/answer/:sessionId', function(request, response) {});

app.post('/text', function(request, response) {});

function textHelper(sessionId) {}

const listener = app.listen(process.env.PORT, function() {
  console.log('Your app is listening on port ' + listener.address().port);
});

Adicionando participantes ao “Pergunte e Responda”

A função mais básica do aplicativo de linha direta é conectar uma pessoa que faz uma pergunta com alguém que se oferece para respondê-la. A maneira mais direta de fazer isso é criar uma sessão de videochamada quando alguém indica que deseja fazer uma pergunta. Em seguida, você pode adicionar a próxima pessoa disponível que queira responder como segundo participante. Seria possível criar uma maneira mais robusta de gerenciar essas sessões ativas usando um banco de dados, mas, para fins de teste, uma matriz deve ser suficiente.

Quando alguém tiver uma dúvida, você pode criar uma nova sessão do OpenTok e armazenar o ID dela no waiting matriz. Em seguida, você pode responder com o novo ID, a chave da API do OpenTok do aplicativo e um token que identifique esse cliente específico:

app.get('/ask', function(request, response) {
  opentok.createSession(function(err, session) {
    let sessionId = session.sessionId;
    waiting.push(sessionId);
    
    response.send({
      apiKey: process.env.OPENTOK_API_KEY,
      sessionId: sessionId,
      token: opentok.generateToken(sessionId)
    });
    
    if (helpers.length) {
      textHelper(sessionId);
    }
  });
});

Você pode ver que a /ask rotina, como etapa final, verifica o tamanho da helpers array. Se encontrar itens nele, ela chama a textHelper função. Vamos discutir textHelper separadamente a seguir, mas se você quiser simplificar sua linha direta conectando apenas as pessoas que estão usando seu aplicativo no momento, pode remover toda essa condição.

Agora, quando alguém se oferecer para responder a uma pergunta, você pode enviar um objeto de resposta semelhante com os valores da primeira sessão na waiting fila. Se ela estiver vazia, você pode enviar uma resposta indicando ao cliente que não há ninguém pedindo ajuda no momento:

app.get('/answer', function(request, response) {
  if (waiting.length) {
    let sessionId = waiting.shift();
    response.send({
      apiKey: process.env.OPENTOK_API_KEY,
      sessionId: sessionId,
      token: opentok.generateToken(sessionId)
    });
  } else {
    response.send({
      wait: true
    });
  }
});

Enviando uma mensagem de texto a um colaborador quando alguém faz uma pergunta

Isso complica um pouquinho nosso fluxo de trabalho atual, mas permitir que possíveis voluntários forneçam seu número de celular e recebam uma mensagem de texto quando uma nova sessão de perguntas estiver pronta ajuda a linha de atendimento a lidar melhor com os períodos de menor movimento.

Salvar o número de telefone de alguém requer apenas algumas linhas de código. O /text endpoint recebe o número de telefone no corpo da solicitação e pode, então, adicioná-lo a uma helpers fila. Em seguida, ele retorna um status “OK”:

app.post('/text', function(request, response) {
  let phone = request.body.phone;
  helpers.push(phone);
  response.sendStatus(200);
});

Agora podemos utilizar o número de telefone armazenado no helper na textHelper função que é chamada pela /ask rota. A função obtém o primeiro número de telefone de helpers e envia a ele, por mensagem de texto, um link para uma sessão específica de videochamada:

function textHelper(sessionId) {
  let phone = helpers.shift();
  
  nexmo.message.sendSms(
    process.env.FROM_PHONE,
    phone,
    'JavaScript question for you! Caller is waiting at: https://' + process.env.PROJECT_DOMAIN + '.glitch.me/?id=' + sessionId
  );
}

Se alguém acessar um link de sessão que recebeu por mensagem de texto, o cliente poderá solicitar ao servidor as credenciais necessárias para participar dessa sessão específica. O aplicativo poderá remover com segurança a sessão da waiting fila assim que ela for efetivamente solicitada:

app.get('/answer/:sessionId', function(request, response) {
  let sessionId = request.params.sessionId;
  let index = waiting.indexOf(sessionId);
  waiting.splice(index, 1);
  response.send({
    apiKey: process.env.OPENTOK_API_KEY,
    sessionId: sessionId,
    token: opentok.generateToken(sessionId)
  });
});

Adicionando uma interface

Para fins de teste, o mais simples é manter todo o seu HTML em uma única página. O exemplo index.html já importa client.js. Acima dessa tag de script, você também precisará importar a biblioteca do cliente OpenTok dos servidores da OpenTok:

<script src="https://static.opentok.com/v2/js/opentok.min.js"></script>

Você pode substituir o conteúdo da <body> tag por três blocos: botões para iniciar o processo de fazer perguntas ou responder, um pseudoformulário para coletar números de telefone de pessoas que gostariam de receber uma mensagem de texto e os destinos dos seus elementos de Video:

    <div id="buttons">
      <a href="/ask" class="bigbutton" id="askBtn">Ask a Question</a>
      <a href="/answer" class="bigbutton" id="answerBtn">Answer a Question</a> 
    </div>
    
    <div id="addNumber">
      No one has a question right now. Want a text when someone does?
      <label>Phone number (e.g. 441234123456):
        <input type="tel" id="phoneNumber" name="phoneNumber" />
      </label>
      <button id="phoneBtn">Text me!</button>
    </div>
    
    <div id="videos">
      <div id="subscriber"></div>
      <div id="publisher"></div>
    </div>

Não vamos abordar o CSS neste tutorial, mas, no mínimo, você provavelmente vai querer ocultar o #addNumber elementos #videos elementos quando a página for carregada pela primeira vez. Você pode adicionar isso e qualquer outro estilo à folha de estilos existente em style.css.

Configurando o lado do cliente

A primeira coisa que você deve fazer no seu script de cliente é verificar se há um id parâmetro na URL. Isso indica que alguém seguiu um link em um texto para participar de uma sessão de bate-papo em andamento. Se houver, você pode obter imediatamente as credenciais necessárias para participar a partir do /answer/:sessionId ponto de extremidade no servidor. O tratamento da resposta do servidor ocorre em uma das duas funções, initializeSession ou handleError, ambas as quais abordaremos em um minuto:

let params = new URLSearchParams(window.location.search);
let ongoingId = params.get('id');
if (ongoingId) {
  fetch('/answer/' + ongoingId).then(function fetch(res) {
    return res.json();
  }).then(function fetchJson(json) {
    initializeSession(json.apiKey, json.sessionId, json.token);
  }).catch(function catchErr(error) {
    handleError(error);
  });
}

Depois de abordar o caso menos comum em que alguém deseja participar de uma sessão específica, você pode configurar o script que será usado para lidar com as ações na interface. Este também é um bom momento para definir handleError. Se desejar, essa função pode ser muito mais complexa do que em sua forma atual, na qual ela apenas envia o erro para o console. Depois disso, você pode selecionar elementos de nível superior que receberão funcionalidades dinâmicas. Se os botões que você selecionou existirem, é possível atribuir a eles manipuladores de clique:

function handleError(error) {
  if (error) {
    console.error(error);
  }
}

var askBtn = document.querySelector('#askBtn');
var answerBtn = document.querySelector('#answerBtn');
var addPhone = document.querySelector('#addNumber');
var phoneBtn = document.querySelector('#phoneBtn');

if (askBtn) askBtn.onclick = askQuestion;
if (answerBtn) answerBtn.onclick = answerQuestion;
if (phoneBtn) phoneBtn.onclick = addPhoneNumber;

Inicializando uma sessão

O processo de inicialização de uma sessão de perguntas e respostas começa ao clicar nos botões “Perguntar” ou “Responder”. Os manipuladores desses botões, askQuestion e answerQuestion, são praticamente idênticos. Primeiro, eles suprimem a ação padrão do link e, em seguida, buscam as credenciais do chat no endpoint apropriado no servidor. Se for possível criar ou participar de uma sessão, initializeSession é chamado com essas credenciais. No caso de answerQuestion, se ninguém estiver fazendo uma pergunta, o helper exibe a opção de fornecer seu número de telefone:

function askQuestion(e) {
  e.preventDefault();
  fetch('/ask').then(function fetch(res) {
    return res.json();
  }).then(function fetchJson(json) {
    initializeSession(json.apiKey, json.sessionId, json.token);
  }).catch(function catchErr(error) {
    handleError(error);
  });
}

function answerQuestion(e) {
  e.preventDefault();
  fetch('/answer').then(function fetch(res) {
    return res.json();
  }).then(function fetchJson(json) {
    if (json.wait) {
      addPhone.style.display = 'block';
      return;
    }
    
    initializeSession(json.apiKey, json.sessionId, json.token);
  }).catch(function catchErr(error) {
    handleError(error);
  });
}

A initializeSession função pode ser a lógica mais complexa de todo o aplicativo. Surpreendentemente, a API do OpenTok está, na verdade, simplificando a lógica necessária. Ela se encarrega da coordenação entre os ouvintes da sessão de bate-papo e os elementos DOM, de modo que tarefas de nível inferior, como criar um <video> elemento e atribuir seu conteúdo, ocorram nos bastidores.

A função primeiro cria uma instância da sessão, fornecendo à API do OpenTok a chave de API e o ID da sessão.

Os métodos estáticos da API do OpenTok no lado do cliente estão disponíveis na OT variável. Você pode ignorar quaisquer avisos do editor sobre OT não estar definida. No entanto, para um aplicativo mais robusto, seria melhor fazer algumas verificações de erros e confirmar se OT ela de fato definida. Por outro lado, você não deve definir OT demais, atribuindo qualquer outro valor no seu código a essa variável, a menos que você altere primeiro o nome padrão do objeto da API.

O primeiro manipulador de que você precisa é para o streamCreated evento. Quando um stream for criado na sessão atual, ele aparecerá no elemento com o ID subscriber. Você também pode adicionar um manipulador para notificar o cliente caso ele seja desconectado da sessão.

Em seguida, defina algumas propriedades para o feed de Video do editor. O cliente é sempre o editor, da própria perspectiva dele. O vídeo dele terá 100% do tamanho e será anexado ao elemento publisher elemento.

Com os manipuladores de eventos e a configuração mínimos já definidos, você pode se conectar à sessão, adicionando o feed do editor ao cliente:

function initializeSession(apiKey, sessionId, token) {
  var session = OT.initSession(apiKey, sessionId);

  // Subscribe to a newly created stream
  session.on('streamCreated', function streamCreated(event) {
    var subscriberOptions = {
      insertMode: 'append',
      width: '100%',
      height: '100%'
    };
    session.subscribe(event.stream, 'subscriber', subscriberOptions, handleError);
  });

  session.on('sessionDisconnected', function sessionDisconnected(event) {
    console.log('You were disconnected from the session.', event.reason);
  });

  // initialize the publisher
  var publisherOptions = {
    insertMode: 'append',
    width: '100%',
    height: '100%'
  };
  var publisher = OT.initPublisher('publisher', publisherOptions, handleError);

  // Connect to the session
  session.connect(token, function callback(error) {
    if (error) {
      handleError(error);
    } else {
      // If the connection is successful, publish the publisher to the session
      session.publish(publisher, handleError);
    }
  });
}

Salvar um número de telefone

Nesse ponto, você já deve ter uma linha direta funcionando. Se alguém clicar no botão “Fazer uma pergunta” e, logo em seguida, outra pessoa clicar em “Responder a uma pergunta”, as duas partes devem entrar em uma videochamada e, com sorte, conseguir resolver todos os seus enigmas de JavaScript. A única coisa que falta adicionar é a possibilidade de enviar um número de telefone para enviar mensagens de texto, caso não haja perguntas no momento, mas surja alguma mais tarde.

O addPhoneNumber manipulador, assim como os manipuladores de botões, apenas suprime o evento padrão e, em seguida, realiza uma consulta. Nesse caso, você POST dados para o servidor, definindo o Content-Type para application/json e transformando o valor do #phoneNumber campo. Se isso der certo, você ocultará o campo de entrada do número de telefone novamente:

function addPhoneNumber(e) {
  e.preventDefault();
  fetch('/text', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({phone: document.querySelector('#phoneNumber').value})
  }).then(() => {
    addPhone.style.display = 'none';
  }).catch(function catchErr(error) {
    handleError(error);
  });
}

Próximos passos

Existem muitos recursos diferentes que você poderia adicionar à sua linha de atendimento, e muitos tipos diferentes de linhas de atendimento que você poderia criar. Seria bom para os usuários verem quantas pessoas estão na fila com perguntas ou respostas, e seria uma vantagem ter a possibilidade de se reconectar a uma sessão que tenha sido interrompida.

Como você já está usando a API da Nexmo, talvez queira adicionar a opção de fazer um simples bate-papo por voz. E, como você já usa o OpenTok, com certeza poderia pensar em adicionar recursos como compartilhamento de tela ou a opção de arquivar perguntas frequentes.

Você pode ler mais sobre o que é possível fazer com o OpenTok no Centro de Desenvolvedores da TokBox, e você pode visualizar e modificar o código deste exemplo no Glitch:

Compartilhar:

https://a.storyblok.com/f/270183/250x250/f231d97f1b/garann-means.png
Garann MeansFormador de Desenvolvedores

Sou desenvolvedor de JavaScript e instrutor de desenvolvimento na Vonage. Ao longo dos anos, tenho me interessado muito por modelos, Node.js, aplicativos web progressivos e estratégias “offline-first”, mas o que sempre adorei de verdade é uma API útil e bem documentada. Meu objetivo é tornar a sua experiência com nossas APIs a melhor possível.