When Dialogflow asks for an Image, then Upload it to Google Drive and then generate Shared Link

When Dialogflow asks for an Image, then Upload it to Google Drive and then generate Shared Link
This commit is contained in:
Leifer Mendez
2022-12-23 19:39:03 +01:00
committed by GitHub
11 changed files with 303 additions and 27 deletions

View File

@@ -10,4 +10,6 @@ SQL_USER=
SQL_PASS=
SQL_DATABASE=
KEEP_DIALOG_FLOW=false
MULTI_DEVICE=true
MULTI_DEVICE=true
DIALOGFLOW_MEDIA_FOR_SLOT_FILLING=false
GDRIVE_FOLDER_ID=

4
.gitignore vendored
View File

@@ -9,4 +9,6 @@ mediaSend/*
!mediaSend/.gitkeep
!mediaSend/nota-de-voz.mp3
.env
.wwebjs_auth
.wwebjs_auth
backup
backup/*

View File

@@ -1,5 +1,6 @@
const dialogflow = require('@google-cloud/dialogflow');
const fs = require('fs')
const {struct} = require('pb-util');
/**
* Debes de tener tu archivo con el nombre "chatbot-account.json" en la raíz del proyecto
@@ -32,6 +33,7 @@ const checkFileCredentials = () => {
// Detect intent method
const detectIntent = async (queryText, waPhoneNumber) => {
let media = null;
let actions = null;
const sessionId = KEEP_DIALOG_FLOW ? 1 : waPhoneNumber;
const sessionPath = sessionClient.projectAgentSessionPath(PROJECID, sessionId);
const languageCode = process.env.LANGUAGE
@@ -54,6 +56,7 @@ const detectIntent = async (queryText, waPhoneNumber) => {
// console.log(singleResponse)
if (parsePayload && parsePayload.payload) {
const { fields } = parsePayload.payload
actions = struct.decode(fields.actions.structValue) || null;
media = fields.media.stringValue || null
}
const customPayload = parsePayload ? parsePayload['payload'] : null
@@ -61,6 +64,7 @@ const detectIntent = async (queryText, waPhoneNumber) => {
const parseData = {
replyMessage: queryResult.fulfillmentText,
media,
actions,
trigger: null
}
return parseData
@@ -74,4 +78,4 @@ const getDataIa = (message = '', sessionId = '', cb = () => { }) => {
checkFileCredentials();
module.exports = { getDataIa }
module.exports = { getDataIa }

103
adapter/gdrive.js Normal file
View File

@@ -0,0 +1,103 @@
require('dotenv').config({ path: `${__dirname}/../.env` });
const { google } = require('googleapis');
const path = require('path');
const fs = require('fs');
//const clientEmail = require(`${__dirname}/../chatbot-account.json`);
/**
* La funcion 'generatePublicUrl' genera un error muy menor al enviar el 'requestBody'
* siempre y cuando necesites que el acceso sea restringido y solo ciertos usuarios puedan acceder.
* Esto se logra con la combinacion requerida: 'reader', 'user' y 'emailAddress':
* requestBody: {
* role: 'reader',
* type: 'user',
* emailAddress: usuario@gmail.com,
* },
* Segun la documentacion https://developers.google.com/drive/api/v3/reference/permissions/create#request-body,
* los datos se envian correctamente, pero la respuesta del API regresa este error:
* Bad Request. User message: "You cannot share this item because it has been flagged as inappropriate."
* Al parecer, es un error conocido en stackoverflow.com entre varios usuarios del API.
*/
if (process.env.DATABASE === 'dialogflow') {
/**
* Debes de tener tu archivo con el nombre "chatbot-account.json" en la raíz del proyecto
*/
const KEYFILEPATH = path.join(`${__dirname}/../chatbot-account.json`);
const SCOPES = ['https://www.googleapis.com/auth/drive'];
const auth = new google.auth.GoogleAuth({
keyFile: KEYFILEPATH,
scopes: SCOPES,
});
const drive = google.drive({
version: 'v3',
auth,
});
const uploadSingleFile = async (fileName, filePath) => {
const folderId = process.env.GDRIVE_FOLDER_ID;
const { data: { id, name } = {} } = await drive.files.create({
resource: {
name: fileName,
parents: [folderId],
},
media: {
mimeType: 'image/jpg',
body: fs.createReadStream(filePath),
},
fields: 'id,name',
});
generatePublicUrl(id).then(() => {
console.log(`Se generó enlace https://drive.google.com/open?id=${id} para el archivo ${name}`);
});
return `https://drive.google.com/open?id=${id}`
};
const scanFolderForFiles = async (folderPath) => {
const folder = await fs.promises.opendir(folderPath);
for await (const dirent of folder) {
if (dirent.isFile() && dirent.name.endsWith('.jpeg')) {
await uploadSingleFile(dirent.name, path.join(folderPath, dirent.name));
await fs.promises.rm(filePath);
}
}
};
async function generatePublicUrl(id) {
try {
const fileId = id;
await drive.permissions.create({
fileId: fileId,
supportsAllDrives: true,
requestBody: {
role: 'reader',
type: 'domain', // 'anyone' da acceso al publico vía enlace https://drive.google.com...
domain: 'gserviceaccount.com', // Si tu cuenta esta bajo un dominio (usuario@empresa.com) y no bajo gmail.com
allowFileDiscovery: false,
},
});
/*
webViewLink: Ver el archivo en el navegador
webContentLink: Enlace de descarga directa
*/
const result = await drive.files.get({
fileId: fileId,
fields: 'webViewLink, webContentLink',
});
console.log(result.data);
} catch (error) {
//console.log(error.message); // Imprime 'Internal Error', pero aún así genera el enlace
console.error = () => { }; // No muestra el error anterior
}
}
module.exports = { uploadSingleFile, scanFolderForFiles }
} else {
console.log(`Actualmente, la base de datos es:\n\t'DATABASE=${process.env.DATABASE}'\nPara usar Google Drive, cambiar a:\n\t'DATABASE=dialogflow'`);
}

52
app.js
View File

@@ -11,8 +11,8 @@ const mysqlConnection = require('./config/mysql')
const { middlewareClient } = require('./middleware/client')
const { generateImage, cleanNumber, checkEnvFile, createClient, isValidNumber } = require('./controllers/handle')
const { connectionReady, connectionLost } = require('./controllers/connection')
const { saveMedia } = require('./controllers/save')
const { getMessages, responseMessages, bothResponse } = require('./controllers/flows')
const { saveMedia, saveMediaToGoogleDrive } = require('./controllers/save')
const { getMessages, responseMessages, bothResponse, waitFor } = require('./controllers/flows')
const { sendMedia, sendMessage, lastTrigger, sendMessageButton, readChat } = require('./controllers/send')
const app = express();
app.use(cors())
@@ -22,6 +22,7 @@ const server = require('http').Server(app)
const port = process.env.PORT || 3000
var client;
var dialogflowFilter = false;
app.use('/', require('./routes/web'))
/**
@@ -46,7 +47,7 @@ const listenMessage = () => client.on('message', async msg => {
/**
* Guardamos el archivo multimedia que envia
*/
if (process.env.SAVE_MEDIA && hasMedia) {
if (process.env.SAVE_MEDIA === 'true' && hasMedia) {
const media = await msg.downloadMedia();
saveMedia(media);
}
@@ -56,9 +57,28 @@ const listenMessage = () => client.on('message', async msg => {
*/
if (process.env.DATABASE === 'dialogflow') {
if (process.env.DIALOGFLOW_MEDIA_FOR_SLOT_FILLING === 'true' && dialogflowFilter) {
waitFor(_ => hasMedia, 30000)
.then(async _ => {
if (hasMedia) {
const media = await msg.downloadMedia();
message = await saveMediaToGoogleDrive(media);
const response = await bothResponse(message.substring(256, -1), number);
await sendMessage(client, from, response.replyMessage);
}
return
});
dialogflowFilter = false;
}
if (!message.length) return;
const response = await bothResponse(message, number);
const response = await bothResponse(message.substring(256, -1), number);
await sendMessage(client, from, response.replyMessage);
if (response.actions) {
await sendMessageButton(client, from, null, response.actions);
return
}
if (response.media) {
sendMedia(client, from, response.media);
}
@@ -124,7 +144,26 @@ const listenMessage = () => client.on('message', async msg => {
}
});
/**
* Este evento es necesario para el filtro de Dialogflow
*/
const listenMessageFromBot = () => client.on('message_create', async botMsg => {
const { body } = botMsg;
const dialogflowFilterConfig = fs.readFileSync('./flow/dialogflow.json', 'utf8');
const keywords = JSON.parse(dialogflowFilterConfig);
for (i = 0; i < keywords.length; i++) {
key = keywords[i];
for (var j = 0; j < key.phrases.length; j++) {
let filters = key.phrases[j];
if (body.includes(filters)) {
dialogflowFilter = true;
//console.log(`El filtro de Dialogflow coincidió con el mensaje: ${filters}`);
}
}
}
});
client = new Client({
authStrategy: new LocalAuth(),
@@ -141,6 +180,7 @@ client.on('qr', qr => generateImage(qr, () => {
client.on('ready', (a) => {
connectionReady()
listenMessage()
listenMessageFromBot()
// socketEvents.sendStatus(client)
});
@@ -155,8 +195,6 @@ client.on('authenticated', () => {
client.initialize();
/**
* Verificamos si tienes un gesto de db
*/
@@ -168,4 +206,4 @@ if (process.env.DATABASE === 'mysql') {
server.listen(port, () => {
console.log(`El server esta listo por el puerto ${port}`);
})
checkEnvFile();
checkEnvFile();

View File

@@ -1,5 +1,5 @@
const {get, reply, getIA} = require('../adapter')
const {saveExternalFile, checkIsUrl} = require('./handle')
const { get, reply, getIA } = require('../adapter')
const { saveExternalFile, checkIsUrl } = require('./handle')
const getMessages = async (message) => {
const data = await get(message)
@@ -8,21 +8,29 @@ const getMessages = async (message) => {
const responseMessages = async (step) => {
const data = await reply(step)
if(data && data.media){
if (data && data.media) {
const file = checkIsUrl(data.media) ? await saveExternalFile(data.media) : data.media;
return {...data,...{media:file}}
return { ...data, ...{ media: file } }
}
return data
}
const bothResponse = async (message, sessionId) => {
const data = await getIA(message, sessionId)
if(data && data.media){
if (data && data.media) {
const file = await saveExternalFile(data.media)
return {...data,...{media:file}}
return { ...data, ...{ media: file } }
}
return data
}
const waitFor = (conditionFunction, WAIT_TIME) => {
const poll = resolve => {
if (conditionFunction())
resolve();
else setTimeout(_ => poll(resolve), WAIT_TIME);
}
return new Promise(poll);
}
module.exports = { getMessages, responseMessages, bothResponse }
module.exports = { getMessages, responseMessages, bothResponse, waitFor }

View File

@@ -1,5 +1,8 @@
const mimeDb = require('mime-db')
const fs = require('fs')
const mimeDb = require('mime-db');
const { uploadSingleFile } = require('../adapter/gdrive');
const fs = require('fs');
var fileName;
/**
* Guardamos archivos multimedia que nuestro cliente nos envie!
@@ -8,11 +11,28 @@ const fs = require('fs')
const saveMedia = (media) => {
const extensionProcess = mimeDb[media.mimetype]
const ext = extensionProcess.extensions[0]
fs.writeFile(`./media/${Date.now()}.${ext}`, media.data, { encoding: 'base64' }, function (err) {
console.log('** Archivo Media Guardado **');
const extensionProcess = mimeDb[media.mimetype];
let ext;
if (!extensionProcess) {
const fileType = media.mimetype.split('/');
ext = fileType[1].split(';')[0];
} else {
ext = extensionProcess.extensions[0];
}
fileName = `${Date.now()}.${ext}`;
fs.writeFile(`./media/${fileName}`, media.data, { encoding: 'base64' }, function (err) {
console.log(`** Archivo Media ${fileName} Guardado **`);
});
return fileName
}
module.exports = {saveMedia}
const saveMediaToGoogleDrive = async (media) => {
fileName = saveMedia(media);
filePath = `${__dirname}/../media/${fileName}`
const googleDriveUrl = await uploadSingleFile(fileName, filePath);
return googleDriveUrl
}
module.exports = { saveMedia, saveMediaToGoogleDrive }

View File

@@ -16,7 +16,7 @@ const { saveMessage } = require('../adapter')
*/
const sendMedia = (client, number = null, fileName = null) => {
if(!client) return cosnole.error("El objeto cliente no está definido.");
if(!client) return console.error("El objeto cliente no está definido.");
try {
number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`;
@@ -36,7 +36,7 @@ const sendMedia = (client, number = null, fileName = null) => {
*/
const sendMediaVoiceNote = (client, number = null, fileName = null) => {
if(!client) return cosnole.error("El objeto cliente no está definido.");
if(!client) return console.error("El objeto cliente no está definido.");
try {
number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`;
@@ -69,12 +69,14 @@ const sendMessage = async (client, number = null, text = null, trigger = null) =
* @param {*} number
*/
const sendMessageButton = async (client, number = null, text = null, actionButtons) => {
setTimeout(async () => {
number = cleanNumber(number)
const { title = null, message = null, footer = null, buttons = [] } = actionButtons;
let button = new Buttons(message,[...buttons], title, footer);
client.sendMessage(number, button);
await readChat(number, message, actionButtons)
console.log(`⚡⚡⚡ Enviando mensajes....`);
}, DELAY_TIME)
}

8
flow/dialogflow.json Normal file
View File

@@ -0,0 +1,8 @@
[
{
"phrases": [
"Se requiere una foto de alguna identificación por razones de seguridad.",
"Por favor envíenos una foto de su ID para completar su formulario."
]
}
]

87
package-lock.json generated
View File

@@ -15,9 +15,11 @@
"exceljs": "^4.3.0",
"express": "^4.18.1",
"file-type": "^17.1.6",
"googleapis": "^109.0.1",
"mime-db": "^1.52.0",
"moment": "^2.29.4",
"mysql": "^2.18.1",
"pb-util": "^1.0.3",
"qr-image": "^3.2.0",
"qrcode-terminal": "^0.12.0",
"socket.io": "^4.5.1",
@@ -2281,6 +2283,42 @@
"node": ">=12.0.0"
}
},
"node_modules/googleapis": {
"version": "109.0.1",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-109.0.1.tgz",
"integrity": "sha512-x286OtNu0ngzxfGz2XgRs4aMhrwutRCkCE12dh2M1jIZOpOndB7ELFXEhmtxaJ7z3257flKIbiiCJZeBO+ze/Q==",
"dependencies": {
"google-auth-library": "^8.0.2",
"googleapis-common": "^6.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/googleapis-common": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz",
"integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==",
"dependencies": {
"extend": "^3.0.2",
"gaxios": "^5.0.1",
"google-auth-library": "^8.0.2",
"qs": "^6.7.0",
"url-template": "^2.0.8",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/googleapis-common/node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -3415,6 +3453,11 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/pb-util": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pb-util/-/pb-util-1.0.3.tgz",
"integrity": "sha512-8+weUH2YEYnPf5sTpZ3q7Drq41tSEL8vDSU96/CzSvu2qrbspbjbbuKLjHocAQpmyMbICTcvovVl3cETwxwIkQ=="
},
"node_modules/peek-readable": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
@@ -4947,6 +4990,11 @@
"safe-buffer": "~5.1.0"
}
},
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7055,6 +7103,35 @@
"node-forge": "^1.3.1"
}
},
"googleapis": {
"version": "109.0.1",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-109.0.1.tgz",
"integrity": "sha512-x286OtNu0ngzxfGz2XgRs4aMhrwutRCkCE12dh2M1jIZOpOndB7ELFXEhmtxaJ7z3257flKIbiiCJZeBO+ze/Q==",
"requires": {
"google-auth-library": "^8.0.2",
"googleapis-common": "^6.0.0"
}
},
"googleapis-common": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz",
"integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==",
"requires": {
"extend": "^3.0.2",
"gaxios": "^5.0.1",
"google-auth-library": "^8.0.2",
"qs": "^6.7.0",
"url-template": "^2.0.8",
"uuid": "^9.0.0"
},
"dependencies": {
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
}
}
},
"graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@@ -7965,6 +8042,11 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"pb-util": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pb-util/-/pb-util-1.0.3.tgz",
"integrity": "sha512-8+weUH2YEYnPf5sTpZ3q7Drq41tSEL8vDSU96/CzSvu2qrbspbjbbuKLjHocAQpmyMbICTcvovVl3cETwxwIkQ=="
},
"peek-readable": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
@@ -9108,6 +9190,11 @@
}
}
},
"url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -36,9 +36,11 @@
"exceljs": "^4.3.0",
"express": "^4.18.1",
"file-type": "^17.1.6",
"googleapis": "^109.0.1",
"mime-db": "^1.52.0",
"moment": "^2.29.4",
"mysql": "^2.18.1",
"pb-util": "^1.0.3",
"qr-image": "^3.2.0",
"qrcode-terminal": "^0.12.0",
"socket.io": "^4.5.1",