
Compartilhar:
Shreyas Sreenivas is a High School Student currently residing in Bangalore, India. He has experience in building applications in JavaScript, TypeScript, Python and PHP. He's also a huge fan of React, GraphQL, Graph Databases and Serverless.
Crie um bot em GraphQL para o WhatsApp e o Messenger para encontrar leitos hospitalares
Tempo de leitura: 13 minutos
A COVID afetou a todos nós, mas alguns mais do que outros. Um dos principais problemas que as pessoas enfrentaram e continuam enfrentando é a escassez de leitos hospitalares. Há alguns meses, criei uma API GraphQL que retorna a disponibilidade de leitos em várias cidades e distritos da Índia. Os dados são extraídos de sites oficiais e normalizados para um formato uniforme, que é então disponibilizado na forma de uma API GraphQL.
Observe que os dados estão disponíveis apenas para algumas cidades da Índia.
Você também pode explorar a API no GraphQL Playground.
Usando a API do Bedav e a Messages API do Vonage, vamos criar um chatbot que funcione com o WhatsApp e o Facebook Messenger usando JavaScript. A função desse chatbot é ajudar os usuários a pesquisar hospitais em uma cidade, encontrar um hospital com leitos disponíveis e fornecer informações adicionais sobre o hospital, como seu número de telefone e endereço. Também criaremos um comando com o qual o usuário poderá obter instruções de como chegar a um hospital.
Pré-requisitos
Um account da Vonage
Configuração do projeto
Embora o código para isso possa ser encontrado no repositório do GitHub, recomenda-se que você siga este tutorial e use o repositório apenas como referência.
Inicializar o NPM e instalar as dependências
Comece criando uma nova pasta, inicializando o npm e criando uma src pasta dentro dela:
Vamos criar nossos webhooks e nosso servidor usando o Express. O Axios será usado para enviar solicitações à Messages API e graphql-request será usado para enviar solicitações à API GraphQL do Bedav. Você pode instalá-los junto com utilitários adicionais executando o seguinte comando:
E, por fim, instale o Nodemon como dependência de desenvolvimento para que não precisemos reiniciar nosso servidor Node.js toda vez que fizermos uma alteração:
Vamos também adicionar um script ao nosso package.json para iniciar nosso servidor de desenvolvimento do Nodemon:
// package.json
{
"scripts": {
"dev": "nodemon src/app.js"
}
} Criar os webhooks
Antes de entrarmos no código, o que são webhooks? Webhooks são, essencialmente, uma URL que é acionada toda vez que uma determinada ação ou evento ocorre. Os webhooks podem ser considerados semelhantes aos callbacks; no entanto, são mais comumente usados para interação entre Applications separadas e independentes. No nosso caso, precisamos disponibilizar dois webhooks: um que é acionado quando o usuário envia uma mensagem e outro quando há uma atualização de status em uma mensagem da API (por exemplo, quando o usuário lê uma mensagem que você enviou).
Primeiro, vamos configurar o Express em src/app.js, onde temos nosso aplicativo principal do Express e as funções principais responsáveis pelo tratamento dos comandos:
// src/app.js
const express = require("express")
const app = express()
app.use(express.json())A seguir, vamos adicionar dois endpoints: um para o webhook de mensagem e outro para o webhook de status:
// src/app.js
const { fixedMessages, sendMessage } = require("./utils")
app.post("/webhooks/inbound", (request, response) => {
console.log(request.body)
)
app.post("/webhooks/status", (request, response) => {
console.log(request.body)
response.status(200).end()
})
app.listen(3000, () => {
console.log("Listening on port 3000")
})
Instalação e configuração do Ngrok
Ngrok permite que você teste seus webhooks, expondo-os à internet por meio de uma URL temporária. Para começar a usar o ngrok, acesse o site deles e criar uma conta. Depois de fazer isso e fazer login, você deverá ver as instruções para instalar e conectar sua Account no painel de controle deles.
Agora que já instalamos e configuramos o ngrok, vamos iniciar nosso servidor de desenvolvimento executando npm run dev. Use o ngrok para criar uma URL pública para o nosso servidor local executando ./ngrok http 3000. Esse comando instrui o ngrok a criar um proxy HTTP para o servidor em execução em localhost:3000.
Configurar o ambiente de teste do Messages
Faça login no Painel de Controle da Vonage e acesse a seção “Sandbox” , na seção Mensagens e Despacho.

Para criar a sandbox do WhatsApp, clique em “Adicionar à sandbox” na seção do WhatsApp. O Sandbox da Vonage precisa verificar sua identidade e, para isso, você pode seguir as etapas indicadas no painel. Você pode seguir etapas semelhantes para criar o Sandbox para o Facebook Messenger.
Você ativou com sucesso o ambiente de teste do WhatsApp! Mas espere, ainda não terminamos. Ainda precisamos fornecer à Vonage a URL onde nossos webhooks estão localizados. Se você rolar a página do ambiente de teste para baixo, deverá ver a seção “Webhooks”, que se parece com o exemplo abaixo:

O primeiro é o inbound webhook que estará na rota webhooks/inbound; portanto, defina esse campo como https://<your-ngrok-https-url>/webhooks/inbound. Da mesma forma, defina a URL do status webhook para https://<your-ngrok-https-url>/webhooks/status. E pronto, já concluímos a configuração da nossa sandbox e do ambiente de teste!
Observação: sempre que você reiniciar o ngrok, será fornecida uma nova URL, que você deverá alterar manualmente no Painel do Vonage.
Enviando uma mensagem de volta ao usuário
Vamos precisar de uma função auxiliar que será usada para enviar mensagens de volta ao usuário.
Primeiro, precisaremos configurar nossas credenciais da API da Vonage para que possamos utilizar a Messages API. Essas credenciais serão armazenadas em variáveis de ambiente. Crie um .env arquivo na raiz da pasta. Para carregar as variáveis de ambiente no ambiente do Node, usaremos o dotenv pacote. Adicione os campos conforme listado abaixo:
VONAGE_API_KEY = // Your Vonage API key
VONAGE_API_SECRET = // Your Vonage API Secret
WHATSAPP_NUMBER = // The from number in the case of the WhatsApp Sandbox
MESSENGER_ID = // The from ID of the Messenger SandboxDepois de fazer isso, adicione o código a seguir no início do arquivo app.js para adicionar as variáveis de ambiente:
// src/app.js
require("dotenv").config()Crie um novo arquivo chamado utils.js na src pasta. Ele conterá nossas funções utilitárias, como formatação e envio de uma mensagem ao usuário.
Faremos uma solicitação à Messages API para enviar uma mensagem. No caso do ambiente de teste (Sandbox) do Messages, a URL da API será https://messages-sandbox.nexmo.com/v0.1/messages. Usaremos o Axios para enviar as solicitações. Podemos usar a auth opção para adicionar nossas credenciais da API, conforme mostrado abaixo:
// src/utils.js
const axios = require("axios")
// default message type will be `text`
const sendMessage = async (to, message, type = "text") => { let from = {
// either 'WhatsApp' or 'messenger'
type: to.type,
}
if (to.type === "whatsapp") {
// the WhatsApp Sandbox number
from.number = proccess.env.WHATSAPP_NUMBER
} else if (to.type === "messenger") {
// the Messenger Sandbox ID
from.id = process.env.MESSENGER_ID
}
await axios.post(
"https://messages-sandbox.nexmo.com/v0.1/messages",
{
to,
from,
message: {
content: {
type,
[type]: message,
},
},
},
{
auth: {
username: process.env.VONAGE_API_KEY,
password: process.env.VONAGE_API_SECRET,
},
}
)
}
module.exports = {
sendMessage,
}
O tipo de mensagem é dinâmico, pois enviaremos uma mensagem do tipo location quando o usuário solicitar instruções de como chegar a um hospital. No WhatsApp, uma mensagem do tipo location anexará uma miniatura do local no Google Maps, juntamente com o nome e o endereço fornecidos.
Criar os comandos
Agora podemos começar a criar nossos comandos:
help- Veja uma lista de todos os comandos que você pode usarcities- Obter uma lista de todas as cidades disponíveissearch <hospital-name> in <location>- Pesquise um hospital em um determinado local. Por exemplo,search sakra in bangalorepesquisas por hospitais com o nome Sakra em Bangalore.get directions to <hospital-id>- Obter instruções de como chegar a um hospital com um ID específico. Por exemplo,get directions to 87enviará a localização do hospital com o ID 87.
O manipulador de mensagens recebidas
Vamos criar uma função que processe as solicitações ao inbound webhook em /webhooks/inbound. Essa função analisa a mensagem enviada pelo usuário e a repassa ao manipulador do comando que o usuário está tentando usar. Ela envia de volta a mensagem apropriada e retorna um código de status 200.
Observe que o webhook deve responder com um código de status 200; caso contrário, a Messages API continuará enviando uma solicitação ao webhook até receber uma resposta 200.
Usamos expressões regulares para verificar se o usuário está tentando usar o search ou directions , e então encaminhamos a mensagem para o manipulador apropriado. Se a mensagem for help, hiou hello, a mensagem de ajuda é enviada ao usuário. Se a mensagem for cities, a lista de cidades disponíveis é enviada ao usuário. E, finalmente, se a mensagem não corresponder a nenhum comando, uma mensagem de erro é enviada ao usuário.
// src/app.js
const handleInbound = async (request, response) => {
const content = request.body.message.content
const text = content.text.toLowerCase().trim()
// whom we have to reply to
const to = request.body.from
const searchRegex = /^search .* in .*$/i
const directionsRegex = /^get directions to .*/i
if (text.match(searchRegex)) {
handleSearch(text, to)
} else if (text.match(directionsRegex)) {
handleDirections(text, to)
} else if (["help", "hi", "hello"].includes(text)) {
sendMessage(to, fixedMessages.help)
} else if (text === "cities") {
sendMessage(to, fixedMessages.cities)
} else {
sendMessage(to, `Sorry, invalid message. Type *help* to get a list of all commands.`)
}
response.status(200).end()
}
Agora, vamos modificar o manipulador do endpoint do webhook de entrada para usar a handleInbound função.
// src/app.js
app.post("/webhooks/inbound", handleInbound) Os comandos “Ajuda” e “Boas-vindas”
Quando o usuário enviar sua primeira mensagem para o nosso serviço, precisaremos de uma mensagem para recebê-lo. Essa mensagem deve conter informações sobre o que nosso bot faz e os diferentes comandos disponíveis. Também precisaremos de uma mensagem de ajuda que será enviada sempre que o usuário não souber o que fazer e digitar help. Essas mensagens podem ser semelhantes.
A API do Bedav contém informações apenas sobre hospitais em determinadas regiões. Também precisaremos de uma mensagem que forneça uma lista das regiões nas quais as informações estão disponíveis.
Conforme implementado na handleInbound função, a mensagem de ajuda é exibida quando o usuário digita hi, “hello”ou help e a mensagem sobre as cidades disponíveis é enviada quando o usuário digita cidades.
Como essas duas mensagens serão constantes, podemos criar um objeto constante no nível superior para definir todas as mensagens fixas. Também usamos outdent para remover os espaços extras em nossa string, que servem como indentação em nosso código. Acrescente o código ao arquivo utils.js:
// src/utils.js
const outdent = require("outdent")
const fixedMessages = {
help: outdent`
The Bedav Bot gives you information on the availability of beds in hospitals and the contact information and location of those hospitals as well.
You can use the following commands:
1. *help* - Get this menu and all the commands you can use
2. *cities* - Get a list of all the cities available
2. *search* _<hospital-name>_ *in* _<location>_ - Search for a hospital in a particular location. For example, "search sakra in bangalore" searches for hospitals with the name Sakra in Bangalore
3. *get directions to* _<hospital-id>_ - Get directions to a hospital with a particular ID. You can get the hospital ID from the search results. The serial number preceding the Hospital name is the Hospital ID. For example, if the search result has _(87) Sakra Hospital_, send _get directions to 87_ to get directions to Sakra Hospital.
`,
cities: outdent`
The cities/districts currently available are:
*Karnataka*
1. Bangalore/Bengaluru
*Maharashtra*
2. Pune
3. Kohlapur
4. Sangli
5. Satara
6. Solapur
*Andhra Pradesh*
7. Anantapur
8. Chittoor
9. East Godavari
10. Guntur
11. Krishna
12. Kurnool
13. Prakasam
14. Nellore
15. Srikakulam
16. Vishakapatanam
17. Vizianagaram
18. West Godavari
19. Kadapa
`,
}
module.exports = {
...
fixedMessages,
} Configurar o cliente GraphQL
Para facilitar as consultas à API GraphQL, vamos usar a graphql-request biblioteca. Para configurar o cliente GraphQL, criamos uma instância de GraphQLClient e fornecemos a URL da API GraphQL.
// app.js
const { GraphQLClient } = require("graphql-request")
const client = new GraphQLClient("https://bedav.org/graphql") Criar algumas funções utilitárias
Conforme mencionado na descrição do hospitalId campo na documentação da API, trata-se de uma string codificada em Base64 que pode ser decodificada para Hospital:<hospitalId> onde hospitalId é um número inteiro exclusivo do hospital. Por motivos que você verá na próxima seção, vamos criar duas funções utilitárias: uma para obter o número do ID do hospital e outra para codificar um número inteiro em uma string Base64 no formato Hospital:<hospitalId>. Usaremos a js-base64 biblioteca que adicionamos ao nosso projeto anteriormente para trabalhar com as base64 cadeias codificadas.
// src/utils.js
const { encode, decode } = require("js-base64")
const getHospitalId = (encodedId) => {
return decode(encodedId).slice(9)
}
const getEncodedString = (hospitalId) => {
return encode(`Hospital:${hospitalId}`)
}
module.exports = {
...
getEncodedString,
}
O comando “Pesquisar”
Funções utilitárias para formatar cadeias de caracteres
Vamos criar mais duas funções utilitárias para formatar os dados dos hospitais: uma para formatar um hospital específico e outra que utilize essa função para formatar um grupo de hospitais.
// src/utils.js
const getFormattedHospital = (hospital) => {
const index = getHospitalId(hospital.id)
const roundedString = (occupied, total) => {
return `${Math.floor((occupied * 100) / total)}% Occupied`
}
const h = hospital
// Percentages of beds available
const percentages = {
icu: roundedString(h.icuOccupied, h.icuTotal),
hdu: roundedString(h.hduOccupied, h.icuTotal),
oxygen: roundedString(h.oxygenOccupied, h.icuTotal),
general: roundedString(h.generalOccupied, h.icuTotal),
ventilators: roundedString(h.ventilatorsOccupied, h.icuTotal),
}
const formatted = outdent`
*(${index}) ${hospital.name}*
${h.icuTotal !== 0 && h.icuAvailable !== null ? `_ICU Available_: ${h.icuAvailable} (${percentages.icu})` : ""}
${h.hduTotal !== 0 && h.icuAvailable !== null ? `_HDU Avalable_: ${h.hduAvailable} (${percentages.hdu})` : ""}
${h.oxygenTotal !== 0 && h.oxygenAvailable !== null ? `_Oxygen Available_: ${h.oxygenAvailable} (${percentages.oxygen}})` : ""}
${h.generalTotal !== 0 && h.generalAvailable !== null ? `_General Available_: ${h.generalAvailable} (${percentages.general})` : ""}
${
h.ventilatorsTotal !== 0 && h.ventilatorsAvailable !== null
? `_Ventilators Available_: ${h.ventilatorsAvailable} (${percentages.ventilators})`
: ""
}
${h.phone !== null ? `_Phone_: ${h.phone}` : ""}
${h.phone !== null ? `_Website_: ${h.website}` : ""}
`
return removeEmptyLines(formatted)
}
Nem todos os hospitais têm leitos disponíveis na UTI, na Unidade de Cuidados Intermediários (UCI) e na enfermaria geral, nem todos dispõem de oxigênio e ventiladores. Se o hospital não tiver um desses itens disponível, podemos omitir essa informação da mensagem. O hospital não tem um determinado tipo de leito, oxigênio ou ventiladores disponíveis se o total disponível for zero ou se o valor do campo “disponível” for nulo.
Também exibimos o código de identificação do hospital, já que o usuário precisa fornecer esse código no comando “obter direções”. Além disso, mostramos ao usuário a porcentagem de leitos ocupados entre parênteses.
Ainda fica uma linha em branco se não houver leito, ventilador ou oxigênio disponível. Para resolver isso, criamos outra função auxiliar para remover essas linhas em branco.
Para verificar se há linhas vazias, usamos uma expressão regular curta que verifica se a linha contém mais do que um único caractere de espaço:
// src/utils.js
const removeEmptyLines = (string) => {
const lines = string.split("\n")
const newLines = []
for (const line of lines) {
// Continue if the line is a blank line
if (line.match(/^\s*$/)) continue
newLines.push(line)
}
return newLines.join("\n")
}
Em seguida, crie a função para formatar uma lista de hospitais, percorrendo todos os hospitais em um loop, obtendo a string formatada para cada um deles e adicionando uma linha em branco entre cada hospital:
// src/utils.js
const getFormattedHospitals = (hospitals) => {
let message = ""
for (const hospital of hospitals) {
const formattedHospital = getFormattedHospital(hospital)
message += formattedHospital + "\n\n"
}
return message
}
module.exports = {
...
getFormattedHospitals,
}
O manipulador de comandos
Todas as consultas GraphQL serão armazenadas no queries.js arquivo na src pasta.
Usamos a consulta GraphQL abaixo para procurar um hospital em um determinado local e recuperar os dados necessários. O name argumento do campo “locality” é usado para informar à API quais dados de localização estamos consultando. O first argumento do hospitals campo especifica quantos hospitais queremos que a API retorne, e o searchQuery fornece a consulta de busca.
// src/queries.js
const searchGraphQLQuery = gql`
query($location: String, $query: String) {
# get hospitals from a city named Bengaluru in the state of Karnataka
locality(name: $location) {
hospitals(first: 5, searchQuery: $query) {
edges {
node {
id
name
phone
website
address
latitude
longitude
icuAvailable
hduAvailable
oxygenAvailable
generalAvailable
ventilatorsAvailable
icuOccupied
hduOccupied
oxygenOccupied
generalOccupied
ventilatorsOccupied
icuTotal
hduTotal
oxygenTotal
generalTotal
ventilatorsTotal
}
}
}
}
}
`
module.exports = {
searchGraphQLQuery,
}
`Observe que não precisamos envolver nossa consulta GraphQL com a
gql. No entanto, com as extensões e ferramentas certas, podemos obter um bom realce de sintaxe e verificação de tipos.
Em seguida, precisamos mapear os nomes dos locais para o name argumento do locality campo, que tem o formato <city/district_name>-<state_name>. O mapeamento da city/district para a localidade name pode ser convertido no objeto abaixo em JavaScript:
// src/utils.js
const locationKey = {
bangalore: "bengaluru-karnataka",
bengaluru: "bengaluru-karnataka",
pune: "pune-maharashtra",
kohlapur: "kohlapur-maharashtra",
sangli: "sangli-maharashtra",
satara: "satara-maharashtra",
solapur: "solapur-maharashtra",
anantapur: "anantapur-andhra pradesh",
chittoor: "chittoor-andhra pradesh",
"east godavari": "east godavari-andhra pradesh",
guntur: "guntur-andhra pradesh",
krishna: "krishna-andhra pradesh",
kurnool: "kurnool-andhra pradesh",
prakasam: "prakasam-andhra pradesh",
nellore: "spsr nellore-andhra pradesh",
srikakulam: "srikakulam-andhra pradesh",
vishakapatanam: "vishakapatanam-andhra pradesh",
vizianagaram: "vizianagaram-andhra pradesh",
"west godavari": "west godavari-andhra pradesh",
kadapa: "kadapa-andhra pradesh",
}
module.exports = {
...
locationKey,
}O texto que o usuário digitar para pesquisar um hospital em um determinado local terá o formato search <search-query> in <location>. Vamos criar a handleSearch função que obtém a consulta de busca e a localização a partir da mensagem do usuário. Essa função é responsável por executar a consulta de busca e enviar a resposta adequada de volta ao usuário:
// src/app.js
const { fixedMessages, sendMessage, locationKey, getFormattedHospitals } = require("./utils")
const handleSearch = async (message, to) => {
// extract the search query and location from the message
const searchRegex = /^search (?<searchQuery>[a-zA-Z0-9_ ]+) in (?<location>[a-zA-Z0-9_ ]+)$/i
const match = searchRegex.exec(message)
if (match === null) {
sendMessage(to, "Please enter the hospital name you want to search for and the location")
return
}
const { groups: { searchQuery, location } } = match
if (!Object.keys(locationKey).includes(location)) {
sendMessage(to, "Invalid location entered. Type *cities* to look at all the cities available")
return
}
try {
const data = await client.request(searchGraphQLQuery, {
location: locationKey[location],
query: searchQuery,
})
const { edges } = data.locality.hospitals
const hospitals = edges.map((item) => item.node)
if (edges.length === 0) {
sendMessage(to, "Sorry, there were no hospitals that matched your search 🙁")
return
}
const formattedMessage = getFormattedHospitals(hospitals)
sendMessage(to, formattedMessage)
} catch (error) {
sendMessage(to, "Sorry, there were no hospitals that matched your search 🙁")
}
} O comando “Obter direções”
O último comando que resta implementar é o comando “get directions”. O formato do comando é get directions to <hospital-id>.
Primeiro, crie a consulta que irá obter as informações do hospital necessárias para enviar a localização do hospital ao usuário:
// src/queries.js
const directionsGraphQLQuery = gql`
query($id: ID!) {
hospital(id: $id) {
id
longitude
latitude
name
address
}
}
`
module.exports = {
...
directionsGraphQLQuery,
}Agora, vamos criar uma handleDirections função que enviará as instruções de como chegar ao hospital solicitado pelo usuário. Se o usuário estiver usando o WhatsApp, enviamos uma mensagem do tipo location que inclui o nome e o endereço do hospital. Como não existe uma mensagem desse tipo location no Messenger, enviamos uma mensagem do tipo text que contém um link do Google Maps para a localização do hospital:
// src/app.js
const handleDirections = async (message, to) => {
const directionsRegex = /^get directions to (?<hospitalId>\d+)$/i
const match = directionsRegex.exec(message)
if (match === null) {
sendMessage(to, "Please enter a valid Hospital ID")
return
}
const hospitalId = getEncodedString(parseInt(match.groups.hospitalId))
try {
const { hospital } = await client.request(directionsGraphQLQuery, {
id: hospitalId,
})
if (to.type === "whatsapp") {
sendMessage(
to,
{
type: "location",
location: {
longitude: hospital.longitude,
latitude: hospital.latitude,
name: hospital.name,
address: hospital.address,
},
},
"custom"
)
} else {
const link = `https://maps.google.com/maps?q=${hospital.latitude},${hospital.longitude}`
const message = `${link}\n*${hospital.name}*\n${hospital.address}\n`
sendMessage(to, message)
}
} catch (error) {
sendMessage(to, "Please enter a valid Hospital ID")
}
}E é isso, pronto! Você pode testar todos os comandos por conta própria na Sandbox, e o resultado deve ficar mais ou menos como na imagem abaixo!

O que vem a seguir
No momento, nosso webhook está aberto para toda a internet. Precisamos de uma maneira de garantir que apenas a Messages API do Vonage possa acessar o endpoint. Saiba mais sobre limitar o acesso usando tokens JWT.
Se você se interessa por Aprendizado de Máquina, poderia usar o Processamento de Linguagem Natural para tornar o bot mais natural de usar.
Usar o ID do hospital no comando “obter direções” não é a melhor experiência para o usuário. Em vez disso, tente implementar um comando em que o usuário digite o nome do hospital para o qual deseja obter o trajeto. Se houver vários hospitais com nomes semelhantes, exiba uma lista dos hospitais juntamente com seus IDs e peça ao usuário para digitar o ID do hospital para o qual deseja obter o trajeto.
O código final do tutorial pode ser encontrado no GitHub.