Commit be3049ab authored by Sebastián Long's avatar Sebastián Long

Finish CHATBOTS-15

parents 4a1e1763 a70c76b9
......@@ -13,3 +13,5 @@ jspm_packages/
/.idea/modules.xml
/.idea/vcs.xml
/.idea/workspace.xml
/.idea
/logs
\ No newline at end of file
# Crisp Chatbot
Este proyecto es un servidor escrito en NodeJS que brinda la interacción de usuario para los chatbots
de reserva de turnos. Se basa en un servicio de eventos, los cuales recibe de Crisp cada vez que el usuario escribe
un mensaje. Cuando esto sucede, interceptamos el mensaje y manejamos los datos del usuario, sobre todo
para saber en qué paso se encuentra dentro de la conversación, y los datos que fue ingresando.
Actualmente el chatbot se despliega en dos sitios web: reserva de turnos bajo demanda (IPS), y
reserva de turnos programados.
El chatbot de reserva de turnos programados se carga en el sitio de Integrando Pacientes.
El script que lo carga (configurado en el proyecto de dicha aplicación web), postea
además los datos del usuario en la API de Crisp, tales como su nombre, apellido y teléfono.
Dichos datos son recuperados por este servidor la primera vez que el usuario interacciona
con el chatbot de reserva de turnos programados, y los envía a Sendinblue. En Sendinblue
tenemos una lista de correos donde enviamos tutoriales de cómo usar el chatbot.
## Configuración
Los siguientes parametros deben ser configurados en [default.json](./config/default.json)
* credenciales.crisp.identifier: identifier para la integración con Crisp.
* credenciales.crisp.key: key para la integración con Crisp.
* credenciales.sendinblue.apikey: API Key de Sendinblue.
* credenciales.isApiToken: API Token de Integrando Salud.
* isBaseUrl: URL base de Integrando Salud (https://www.integrandosalud.com/src-v2/public/api/v1)
* sendingbluBaseUrl: URL base de Sendingblue (https://api.sendinblue.com/v3)
* sendingblueListId: ID de la lista de correo de Sendinblue (54)
* crispWebsiteIdIpsTurnosBajoDemanda: Website ID para el website de turnos bajo demanda.
* crispWebsiteIdIsTurnosProgramados Website ID para el website de turnos programados.
* idAgendaIPS: ID de la agenda del IPS.
* idDominioIPS: ID del dominio del IPS.
## Ejecución
Para correr el proyecto, una vez seteada la configuración, correr el siguiente comando:
```npm run start```
## Logs
Los logs se almacenan en el directorio [/logs](./logs).
* El archivo [error.log](./logs/error.log) muestra solamente errores.
* El archivo [info.log](./logs/error.log) muestra tanto errores como logs de información.
\ No newline at end of file
var Crisp = require("node-crisp-api");
var CrispClient = new Crisp();
const Crisp = require("crisp-api");
const CrispClient = new Crisp();
const config = require('config');
var identifier = config.get('credenciales.crisp.identifier');
var key = config.get('credenciales.crisp.key');
let SIGUIENTE_PASO = 'siguiente_paso';
const identifier = config.get('credenciales.crisp.identifier');
const key = config.get('credenciales.crisp.key');
const SIGUIENTE_PASO = 'siguiente_paso';
const DELAY_MS = 1000;
let localData = {};
CrispClient.authenticate(identifier, key);
CrispClient.authenticateTier("plugin", identifier, key);
async function sendTextMessage(website_id, session_id, message) {
await composeMessage(website_id, session_id, DELAY_MS);
await CrispClient.websiteConversations.sendMessage(
await CrispClient.website.sendMessageInConversation(
website_id,
session_id, {
type: "text",
......@@ -25,14 +25,14 @@ async function sendTextMessage(website_id, session_id, message) {
}
async function composeMessage(website_id, session_id, ms){
await CrispClient.websiteConversations.composeMessage(
await CrispClient.website.composeMessageInConversation(
website_id,
session_id, {
"type": "start",
"from": "operator"}
);
await new Promise(resolve => setTimeout(resolve, ms));
await CrispClient.websiteConversations.composeMessage(
await CrispClient.website.composeMessageInConversation(
website_id,
session_id, {
"type": "stop",
......@@ -46,7 +46,7 @@ async function showWritingIcon(website_id, session_id){
async function sendPickerMessage(website_id, session_id, id, title, choices) {
await composeMessage(website_id, session_id, DELAY_MS);
await CrispClient.websiteConversations.sendMessage(
await CrispClient.website.sendMessageInConversation(
website_id,
session_id, {
type: "picker",
......@@ -84,7 +84,7 @@ async function getUserData(website_id, session_id) {
}
async function updateCrispData(website_id, session_id, correo, tipo_documento, numero_documento, fecha_nacimiento, sexo) {
await CrispClient.websiteConversations.updateMeta(website_id, session_id, {
await CrispClient.website.updateConversationMetas(website_id, session_id, {
email: correo,
data: {
tipo_documento: tipo_documento,
......@@ -96,7 +96,7 @@ async function updateCrispData(website_id, session_id, correo, tipo_documento, n
}
async function getCrispData(website_id, session_id) {
return await CrispClient.websiteConversations.getMeta(website_id, session_id);
return await CrispClient.website.getConversationMetas(website_id, session_id);
}
async function updateUserData(website_id, session_id, data) {
......
const config = require('config');
const axios = require('axios');
let _ = require('lodash');
const logger = require('../logger/index.js');
var AUTH_TOKEN = config.get('credenciales.isApiToken');
const AUTH_TOKEN = config.get('credenciales.isApiToken');
let isClient = axios.create({
baseURL: config.get('isBaseUrl'),
......@@ -22,14 +23,6 @@ function sortProvinciasByName(provincias){
return provinciasByName;
}
function getProvinciaById(provincias, id) {
for (let i = 0; i < provincias.length; i++) {
if (provincias[i].id_provincia === id) {
return provincias[i];
}
}
}
module.exports =
{
getProvincias: async function () {
......@@ -39,7 +32,7 @@ module.exports =
return sortProvinciasByName(provincias);
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
getMedicosByName: async function (nombreMedico, idProvincia) {
......@@ -48,7 +41,7 @@ module.exports =
return response.data.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
getEspecialidadesMedico: async function (idPersonaInstitucional, idProvincia) {
......@@ -57,7 +50,7 @@ module.exports =
return response.data.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
getTurnosDisponibles: async function (idAgenda, idPersonaInstitucional, fechaUsa) {
......@@ -66,7 +59,7 @@ module.exports =
return response.data.turnos;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
getSiguientesTurnosDisponibles: async function (idAgenda, idPersonaInstitucional) {
......@@ -75,7 +68,7 @@ module.exports =
return response.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
existeUsuario: async function(tipo_documento, numero_documento, fecha_nacimiento, sexo){
......@@ -84,7 +77,7 @@ module.exports =
return response.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
});
},
crearTurno: async function (idPersonaFederada, idHorario, fechaHora) {
......@@ -97,7 +90,8 @@ module.exports =
return response.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
return error;
});
},
federarUsuario: async function (tipoDocumento, numeroDocumento, fechaNacimiento, sexo, mail, telefonoCelular, idDominio) {
......@@ -119,7 +113,7 @@ module.exports =
return response.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
return {success: false};
});
},
......@@ -129,7 +123,7 @@ module.exports =
return response.data;
})
.catch(function (error) {
console.log(error);
logger.error(error.data);
return {success: false};
});
}
......
const config = require('config');
const axios = require('axios');
const logger = require('../logger/index.js');
const API_KEY = config.get('credenciales.sendinblue.apiKey');
const COUNTRY_CODE_ARG = "+54";
......@@ -13,12 +14,12 @@ let sendinblueClient = axios.create({
async function saveContactToList(email, nombres, apellidos, sms, listId){
sms = parseSms(sms);
createOrUpdateContact(email, nombres, apellidos, sms)
.then(function (data){
console.log(`Usuario ${email} creado/actualizado`);
.then(function (){
logger.info(`Usuario ${email} creado/actualizado`);
addContactToList(email, listId);
})
.catch(function (error) {
console.log(`Error al crear/actualizar usuario en sendinblue: ${JSON.stringify(error.response.data)}`);
logger.error(`Error al crear/actualizar usuario en sendinblue: ${JSON.stringify(error.response.data)}`);
});
}
......@@ -40,7 +41,6 @@ async function createOrUpdateContact(email, nombres, apellidos, sms){
})
.catch(async function (error) {
if(error.response.data.code === ERROR_CODE_DUPLICATE_PARAMETER){
console.log(error.response.data)
return await sendinblueClient.put(encodeURI(`/contacts/${email}`), {
"attributes": {
"NOMBRE": nombres,
......@@ -52,6 +52,10 @@ async function createOrUpdateContact(email, nombres, apellidos, sms){
return response.data;
})
}
else{
logger.error(error.data);
return error;
}
});
}
......@@ -60,11 +64,12 @@ async function addContactToList(email, listId){
"emails": [email]
})
.then(async function (response) {
console.log(`Usuario ${email} agregado a lista ${listId}`)
logger.info(`Usuario ${email} agregado a lista ${listId}`)
return response.data;
})
.catch(async function (error) {
console.log(`Error al agregar al usuario ${email} a la lista ${listId}: ${JSON.stringify(error.response.data)}`);
logger.error(`Error al agregar al usuario ${email} a la lista ${listId}:`);
logger.error(error.response.data);
});
}
......
const axios = require('axios');
const config = require('config');
const handlerTurnosProgramados = require('./handlers/is_turnos_programados.js');
const handlerTurnosBajoDemanda = require('./handlers/ips_turnos_bajo_demanda');
const crisp = require('./crisp.js');
const utils = require('./utils.js');
const pasosTurnosProgramados = require('./handlers/pasos_is_turnos_programados.js');
const pasosIpsTurnosBajoDemanda = require('./handlers/pasos_ips_turnos_bajo_demanda');
const is = require('./endpoints/is.js');
const sendinblue = require('./endpoints/sendinblue.js');
const logger = require('./logger/index.js');
const WEBSITE_ID_IPS_TURNOS_BAJO_DEMANDA = config.get('crispWebsiteIdIpsTurnosBajoDemanda');
const WEBSITE_ID_TURNOS_PROGRAMADOS = config.get('crispWebsiteIdIsTurnosProgramados');
const REINICIAR = 'REINICIAR';
crisp.CrispClient.userProfile.get().then(function(myProfile) {
console.log(`El chatbot esta escuchando eventos (profile name: ${myProfile.first_name})`);
});
crisp.CrispClient.plugin.getConnectAccount()
.then(account => {
console.log('El chatbot esta escuchando eventos');
logger.info('Servicio iniciado');
logger.info("Plugin ID:");
logger.info(account.plugin_id);
})
.catch(error => {
console.error('Error al inicializar el chatbot: ', error);
logger.error('Error al inicializar el chatbot:');
logger.error(error);
});
crisp.CrispClient.on("message:updated", async function (message) {
logger.info('Opcion del usuario:');
logger.info(message.content);
darSiguientePaso(message, message.website_id, message.session_id);
})
crisp.CrispClient.on("message:send", async function (message) {
if(message.content.toUpperCase() === 'REINICIAR'){
if(message.content.toUpperCase() === REINICIAR){
await crisp.updateUserData(message.website_id, message.session_id, {});
}
let siguientePaso = await crisp.getSiguientePaso(message.website_id, message.session_id);
logger.info(`Usuario tipea: ${message.content}`)
if (siguientePaso == null) {
if(message.website_id === WEBSITE_ID_TURNOS_PROGRAMADOS){
enviarUserAListaSendingblue(message.website_id, message.session_id, config.get('sendingblueListId'));
......@@ -36,7 +47,12 @@ crisp.CrispClient.on("message:send", async function (message) {
}
darSiguientePaso(message, message.website_id, message.session_id)
.catch((ignorar) => {}) //Ignorar error cuando el usuario escribe un mensaje en lugar de usar el picker, o viceversa
.catch((error) => {
logger.error(`Error al procesar el mensaje del usuario:`);
logger.error(error);
crisp.sendTextMessage(message.website_id, message.session_id,
`Ocurrió un error al procesar su respuesta. Por favor, verifique los datos ingresados y reintente. Si quiere comenzar desde el inicio, escriba ${REINICIAR}.`)
})
});
async function darSiguientePaso(message, website_id, session_id) {
......@@ -59,9 +75,10 @@ async function enviarUserAListaSendingblue(website_id, session_id, listId){
let email = crispUsrData['email'];
let phone = crispUsrData['phone'];
console.log(nombres, apellidos, email, phone);
logger.info(nombres, apellidos, email, phone);
sendinblue.saveContactToList(email, nombres, apellidos, phone, listId);
} catch (e) {
console.log(`Hubo un error al intentar enviar el usuario a la lista de sendinblue. Error: ${e}`);
logger.error(`Hubo un error al intentar enviar el usuario a la lista de sendinblue. Error:`);
logger.error(e);
}
}
\ No newline at end of file
const winston = require('winston');
const { combine, timestamp, printf } = winston.format;
const logFormat = printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${JSON.stringify(message)}`;
});
const logger = winston.createLogger({
level: 'info',
format: combine(
timestamp(),
logFormat
),
defaultMeta: { service: 'chatbot' },
transports: [
// Guardar logs con nivel `error` y por debajo en `error.log`
new winston.transports.File({ filename: 'logs/error.log', level: 'error', handleRejections: true }),
// Guardar logs con nivel `info` y por debajo en `info.log`
new winston.transports.File({ filename: 'logs/info.log' }),
],
});
module.exports = logger;
\ No newline at end of file
This diff is collapsed.
......@@ -11,10 +11,11 @@
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"axios": "^0.21.4",
"config": "^3.3.6",
"crisp-api": "5.0.1",
"lodash": "^4.17.21",
"node-crisp-api": "^1.12.2"
"winston": "^3.3.3"
},
"devDependencies": {
"nodemon": "^2.0.7"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment