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

@@ -11,3 +11,5 @@ SQL_PASS=
SQL_DATABASE= SQL_DATABASE=
KEEP_DIALOG_FLOW=false KEEP_DIALOG_FLOW=false
MULTI_DEVICE=true MULTI_DEVICE=true
DIALOGFLOW_MEDIA_FOR_SLOT_FILLING=false
GDRIVE_FOLDER_ID=

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ mediaSend/*
!mediaSend/nota-de-voz.mp3 !mediaSend/nota-de-voz.mp3
.env .env
.wwebjs_auth .wwebjs_auth
backup
backup/*

View File

@@ -1,5 +1,6 @@
const dialogflow = require('@google-cloud/dialogflow'); const dialogflow = require('@google-cloud/dialogflow');
const fs = require('fs') 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 * 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 // Detect intent method
const detectIntent = async (queryText, waPhoneNumber) => { const detectIntent = async (queryText, waPhoneNumber) => {
let media = null; let media = null;
let actions = null;
const sessionId = KEEP_DIALOG_FLOW ? 1 : waPhoneNumber; const sessionId = KEEP_DIALOG_FLOW ? 1 : waPhoneNumber;
const sessionPath = sessionClient.projectAgentSessionPath(PROJECID, sessionId); const sessionPath = sessionClient.projectAgentSessionPath(PROJECID, sessionId);
const languageCode = process.env.LANGUAGE const languageCode = process.env.LANGUAGE
@@ -54,6 +56,7 @@ const detectIntent = async (queryText, waPhoneNumber) => {
// console.log(singleResponse) // console.log(singleResponse)
if (parsePayload && parsePayload.payload) { if (parsePayload && parsePayload.payload) {
const { fields } = parsePayload.payload const { fields } = parsePayload.payload
actions = struct.decode(fields.actions.structValue) || null;
media = fields.media.stringValue || null media = fields.media.stringValue || null
} }
const customPayload = parsePayload ? parsePayload['payload'] : null const customPayload = parsePayload ? parsePayload['payload'] : null
@@ -61,6 +64,7 @@ const detectIntent = async (queryText, waPhoneNumber) => {
const parseData = { const parseData = {
replyMessage: queryResult.fulfillmentText, replyMessage: queryResult.fulfillmentText,
media, media,
actions,
trigger: null trigger: null
} }
return parseData return parseData

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 { middlewareClient } = require('./middleware/client')
const { generateImage, cleanNumber, checkEnvFile, createClient, isValidNumber } = require('./controllers/handle') const { generateImage, cleanNumber, checkEnvFile, createClient, isValidNumber } = require('./controllers/handle')
const { connectionReady, connectionLost } = require('./controllers/connection') const { connectionReady, connectionLost } = require('./controllers/connection')
const { saveMedia } = require('./controllers/save') const { saveMedia, saveMediaToGoogleDrive } = require('./controllers/save')
const { getMessages, responseMessages, bothResponse } = require('./controllers/flows') const { getMessages, responseMessages, bothResponse, waitFor } = require('./controllers/flows')
const { sendMedia, sendMessage, lastTrigger, sendMessageButton, readChat } = require('./controllers/send') const { sendMedia, sendMessage, lastTrigger, sendMessageButton, readChat } = require('./controllers/send')
const app = express(); const app = express();
app.use(cors()) app.use(cors())
@@ -22,6 +22,7 @@ const server = require('http').Server(app)
const port = process.env.PORT || 3000 const port = process.env.PORT || 3000
var client; var client;
var dialogflowFilter = false;
app.use('/', require('./routes/web')) app.use('/', require('./routes/web'))
/** /**
@@ -46,7 +47,7 @@ const listenMessage = () => client.on('message', async msg => {
/** /**
* Guardamos el archivo multimedia que envia * Guardamos el archivo multimedia que envia
*/ */
if (process.env.SAVE_MEDIA && hasMedia) { if (process.env.SAVE_MEDIA === 'true' && hasMedia) {
const media = await msg.downloadMedia(); const media = await msg.downloadMedia();
saveMedia(media); saveMedia(media);
} }
@@ -56,9 +57,28 @@ const listenMessage = () => client.on('message', async msg => {
*/ */
if (process.env.DATABASE === 'dialogflow') { if (process.env.DATABASE === 'dialogflow') {
if (!message.length) return;
const response = await bothResponse(message, number); 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); await sendMessage(client, from, response.replyMessage);
}
return
});
dialogflowFilter = false;
}
if (!message.length) return;
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) { if (response.media) {
sendMedia(client, from, 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({ client = new Client({
authStrategy: new LocalAuth(), authStrategy: new LocalAuth(),
@@ -141,6 +180,7 @@ client.on('qr', qr => generateImage(qr, () => {
client.on('ready', (a) => { client.on('ready', (a) => {
connectionReady() connectionReady()
listenMessage() listenMessage()
listenMessageFromBot()
// socketEvents.sendStatus(client) // socketEvents.sendStatus(client)
}); });
@@ -155,8 +195,6 @@ client.on('authenticated', () => {
client.initialize(); client.initialize();
/** /**
* Verificamos si tienes un gesto de db * Verificamos si tienes un gesto de db
*/ */

View File

@@ -1,5 +1,5 @@
const {get, reply, getIA} = require('../adapter') const { get, reply, getIA } = require('../adapter')
const {saveExternalFile, checkIsUrl} = require('./handle') const { saveExternalFile, checkIsUrl } = require('./handle')
const getMessages = async (message) => { const getMessages = async (message) => {
const data = await get(message) const data = await get(message)
@@ -8,21 +8,29 @@ const getMessages = async (message) => {
const responseMessages = async (step) => { const responseMessages = async (step) => {
const data = await reply(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; const file = checkIsUrl(data.media) ? await saveExternalFile(data.media) : data.media;
return {...data,...{media:file}} return { ...data, ...{ media: file } }
} }
return data return data
} }
const bothResponse = async (message, sessionId) => { const bothResponse = async (message, sessionId) => {
const data = await getIA(message, sessionId) const data = await getIA(message, sessionId)
if(data && data.media){ if (data && data.media) {
const file = await saveExternalFile(data.media) const file = await saveExternalFile(data.media)
return {...data,...{media:file}} return { ...data, ...{ media: file } }
} }
return data 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 mimeDb = require('mime-db');
const fs = require('fs') const { uploadSingleFile } = require('../adapter/gdrive');
const fs = require('fs');
var fileName;
/** /**
* Guardamos archivos multimedia que nuestro cliente nos envie! * Guardamos archivos multimedia que nuestro cliente nos envie!
@@ -8,11 +11,28 @@ const fs = require('fs')
const saveMedia = (media) => { const saveMedia = (media) => {
const extensionProcess = mimeDb[media.mimetype] const extensionProcess = mimeDb[media.mimetype];
const ext = extensionProcess.extensions[0] let ext;
fs.writeFile(`./media/${Date.now()}.${ext}`, media.data, { encoding: 'base64' }, function (err) { if (!extensionProcess) {
console.log('** Archivo Media Guardado **'); 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) => { 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 { try {
number = cleanNumber(number || 0) number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`; const file = `${DIR_MEDIA}/${fileName}`;
@@ -36,7 +36,7 @@ const sendMedia = (client, number = null, fileName = null) => {
*/ */
const sendMediaVoiceNote = (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 { try {
number = cleanNumber(number || 0) number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`; const file = `${DIR_MEDIA}/${fileName}`;
@@ -69,12 +69,14 @@ const sendMessage = async (client, number = null, text = null, trigger = null) =
* @param {*} number * @param {*} number
*/ */
const sendMessageButton = async (client, number = null, text = null, actionButtons) => { const sendMessageButton = async (client, number = null, text = null, actionButtons) => {
setTimeout(async () => {
number = cleanNumber(number) number = cleanNumber(number)
const { title = null, message = null, footer = null, buttons = [] } = actionButtons; const { title = null, message = null, footer = null, buttons = [] } = actionButtons;
let button = new Buttons(message,[...buttons], title, footer); let button = new Buttons(message,[...buttons], title, footer);
client.sendMessage(number, button); client.sendMessage(number, button);
await readChat(number, message, actionButtons)
console.log(`⚡⚡⚡ Enviando mensajes....`); 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", "exceljs": "^4.3.0",
"express": "^4.18.1", "express": "^4.18.1",
"file-type": "^17.1.6", "file-type": "^17.1.6",
"googleapis": "^109.0.1",
"mime-db": "^1.52.0", "mime-db": "^1.52.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"mysql": "^2.18.1", "mysql": "^2.18.1",
"pb-util": "^1.0.3",
"qr-image": "^3.2.0", "qr-image": "^3.2.0",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"socket.io": "^4.5.1", "socket.io": "^4.5.1",
@@ -2281,6 +2283,42 @@
"node": ">=12.0.0" "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": { "node_modules/graceful-fs": {
"version": "4.2.10", "version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "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", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "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": { "node_modules/peek-readable": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz",
@@ -4947,6 +4990,11 @@
"safe-buffer": "~5.1.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -7055,6 +7103,35 @@
"node-forge": "^1.3.1" "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": { "graceful-fs": {
"version": "4.2.10", "version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "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", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" "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": { "peek-readable": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.0.0.tgz", "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

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