From 1a4122ffce4b565a61fee75bc5ac1598051e41ae Mon Sep 17 00:00:00 2001 From: cheveguerra Date: Tue, 27 Dec 2022 02:30:32 -0600 Subject: [PATCH] add templates from main --- .env.example | 15 ++++ .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/bug.yml | 58 ++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 + .github/ISSUE_TEMPLATE/test-case.yml | 79 +++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 17 ++++ .github/workflows/codeql.yml | 76 ++++++++++++++++ .github/workflows/stale.yml | 28 ++++++ .gitignore | 3 +- CODE_OF_CONDUCT.md | 128 +++++++++++++++++++++++++++ LICENSE.md | 21 +++++ SECURITY.md | 21 +++++ adapter/diaglogflow.js | 18 ++-- adapter/gdrive,.js | 103 +++++++++++++++++++++ adapter/gdrive.js | 103 +++++++++++++++++++++ app.js | 53 +++++++++-- controllers/flows.js | 18 ++-- controllers/save.js | 43 ++++++--- controllers/send.js | 1 + flow/dialogflow.json | 8 ++ package-lock.json | 66 ++++++++++---- package.json | 3 +- spam.json | 75 ++++++++-------- 23 files changed, 856 insertions(+), 88 deletions(-) create mode 100644 .env.example create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/test-case.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/stale.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE.md create mode 100644 SECURITY.md create mode 100644 adapter/gdrive,.js create mode 100644 adapter/gdrive.js create mode 100644 flow/dialogflow.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..69d8a2e --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +######DATABASE: none, mysql, dialogflow + +DEFAULT_MESSAGE=true +SAVE_MEDIA=true +PORT=3000 +DATABASE=none +LANGUAGE=es +SQL_HOST= +SQL_USER= +SQL_PASS= +SQL_DATABASE= +KEEP_DIALOG_FLOW=false +MULTI_DEVICE=true +DIALOGFLOW_MEDIA_FOR_SLOT_FILLING=false +GDRIVE_FOLDER_ID= \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..95cd665 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +patreon: leifermendez +custom: "https://www.buymeacoffee.com/leifermendez" +open_collective: bot-whatsapp diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..2d2f5a6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,58 @@ +name: 🐛 Reporte Bug +description: Algo no va bien?. Hazlo saber +labels: [bug, triage] +title: '[🐛]' +body: + - type: markdown + attributes: + value: | + Gracias por tomarte el tiempo de reportar este problema + + - type: dropdown + id: version + attributes: + label: ¿Que versión estas usando? + description: '__INFO:__ Recuerda que puedes consultar dudas directamente en [discord](https://link.codigoencasa.com/DISCORD)' + options: + - v2 + - v1 + validations: + required: true + + - type: dropdown + id: component + attributes: + label: ¿Sobre que afecta? + options: + - Flujo de palabras (Flow) + - DialogFlow + - Base de datos + - Otro + validations: + required: true + + - type: textarea + id: description + attributes: + description: 'Trata de ser lo más claro posible, de esa manera podemos entender el contexto de tu problema y darte una mejor solución' + label: Describe tu problema + placeholder: Yo tengo un problema.... + + validations: + required: true + + - type: input + id: reproduction + attributes: + label: Reproducir error + description: __(Recomendación)__ trata de grabar un video puedes usar algunas de las siguientes herramientas [https://www.vidyard.com/](https://www.vidyard.com/) [https://www.loom.com/](https://www.loom.com/) y en lo posbile apoyate en [https://stackblitz.com/](https://stackblitz.com/) para compartir el código de ser necesario + placeholder: URL video o stackblitz + validations: + required: false + + - type: textarea + id: additional_information + attributes: + label: Información Adicional + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..7590616 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: 🤔 Core Team + url: https://link.codigoencasa.com/DISCORD + about: Si quieres formar parte del CoreTeam, patrocinar el proyecto o propuesta profesionales diff --git a/.github/ISSUE_TEMPLATE/test-case.yml b/.github/ISSUE_TEMPLATE/test-case.yml new file mode 100644 index 0000000..e712610 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test-case.yml @@ -0,0 +1,79 @@ +name: 🐬 Caso de uso +description: Reporta tu caso de uso y cuales fueron tus resultados +labels: [usecase] +title: '[🐬]' +body: + - type: markdown + attributes: + value: | + Gracias por tomarte el tiempo de detallar este caso de uso, sera de gran utilidad para mantener un software de calidad puedes comenzar + ⚡ `npm create bot-whatsapp@dev` + + - type: dropdown + id: version + attributes: + label: ¿Cual proveedor usaste? + description: 'Actualmente tenemos varios proveedores que sirven como punto de entrada y salida con Whatsapp' + options: + - whatsapp-web.js + - venom + - bailey + - twilio + - meta + validations: + required: true + + - type: dropdown + id: component + attributes: + label: ¿Cual base de datos usaste? + options: + - memory + - mongo + - mysql + - json + validations: + required: true + + - type: dropdown + id: result + attributes: + label: Conclusion de la prueba + options: + - muy buena + - buena + - tiene errores + validations: + required: true + + - type: textarea + id: description + attributes: + description: 'Trata de ser lo más claro posible, de esa manera podemos entender el contexto del caso de uso' + label: Describe tu caso + placeholder: Yo tengo un caso.... + validations: + required: true + + - type: textarea + id: logs + attributes: + label: ¿Logs Importantes? + description: Si tienes algunos logs importantes a tener en cuenta o que muetren algun error en concreto. + render: shell + + - type: textarea + id: additional_information + attributes: + label: Información Adicional + validations: + required: false + + - type: input + id: usernames + attributes: + label: ¿Quieres que te mencionemos? + description: Siempre buscamos fomentar la comunidad por lo cual si quieres que te mencionemos publicamente en nuestras redes sociales puedes dejar tu username + placeholder: twitter o github o instagram o alguna url + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8278042 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +# Que tipo de Pull Request es? + +- [ ] Mejoras +- [ ] Bug +- [ ] Docs / tests + +# Descripción + +Por favor agrega una descripción de tu aporte para tener más contexto y poder avanzar más rápido. Si es de ayuda puedes usar plataformar como [https://www.loom.com/](https://www.loom.com/) para grabar un video. + + +> Forma parte de este proyecto. + +- [Discord](https://link.codigoencasa.com/DISCORD) +- [Twitter](https://twitter.com/leifermendez) +- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR) +- [Telegram](https://t.me/leifermendez) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9310bcc --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,76 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main", dev, next-release ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '21 16 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..0996017 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,28 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Revisar ISSUES abandonadas + +on: + schedule: + - cron: '55 22 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: '¿Alguna novedad sobre esta ISSUE?' + stale-pr-message: '¿Alguna novedad sobre esta PULL REQUEST?' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' + exempt-issue-assignees: 'leifermendez' diff --git a/.gitignore b/.gitignore index a61ee4c..4a4afc4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ mediaSend/* !mediaSend/nota-de-voz.mp3 .env .wwebjs_auth -spam_* \ No newline at end of file +backup +backup/* \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..05743b3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +leifer.contacto@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..959d8ec --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Leifer Mendez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..034e848 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/adapter/diaglogflow.js b/adapter/diaglogflow.js index 9a243b5..9a0a641 100644 --- a/adapter/diaglogflow.js +++ b/adapter/diaglogflow.js @@ -1,6 +1,7 @@ const dialogflow = require('@google-cloud/dialogflow'); const fs = require('fs') -const { nanoid } = require('nanoid') +const {struct} = require('pb-util'); + /** * Debes de tener tu archivo con el nombre "chatbot-account.json" en la raíz del proyecto */ @@ -30,9 +31,10 @@ const checkFileCredentials = () => { // Detect intent method -const detectIntent = async (queryText) => { +const detectIntent = async (queryText, waPhoneNumber) => { let media = null; - const sessionId = KEEP_DIALOG_FLOW ? 1 : nanoid(); + let actions = null; + const sessionId = KEEP_DIALOG_FLOW ? 1 : waPhoneNumber; const sessionPath = sessionClient.projectAgentSessionPath(PROJECID, sessionId); const languageCode = process.env.LANGUAGE const request = { @@ -54,24 +56,26 @@ const detectIntent = async (queryText) => { // 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['payload'] + const customPayload = parsePayload ? parsePayload['payload'] : null const parseData = { replyMessage: queryResult.fulfillmentText, media, + actions, trigger: null } return parseData } -const getDataIa = (message = '', cb = () => { }) => { - detectIntent(message).then((res) => { +const getDataIa = (message = '', sessionId = '', cb = () => { }) => { + detectIntent(message, sessionId).then((res) => { cb(res) }) } checkFileCredentials(); -module.exports = { getDataIa } +module.exports = { getDataIa } \ No newline at end of file diff --git a/adapter/gdrive,.js b/adapter/gdrive,.js new file mode 100644 index 0000000..15be1b7 --- /dev/null +++ b/adapter/gdrive,.js @@ -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'`); +} \ No newline at end of file diff --git a/adapter/gdrive.js b/adapter/gdrive.js new file mode 100644 index 0000000..2e3ad30 --- /dev/null +++ b/adapter/gdrive.js @@ -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'`); +} diff --git a/app.js b/app.js index dd6c559..4d49a57 100644 --- a/app.js +++ b/app.js @@ -13,8 +13,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, sendMessageList, readChat } = require('./controllers/send'); const { remplazos, stepsInitial} = require('./adapter/index');//MOD by CHV - Agregamos para utilizar remplazos y stepsInitial const { isUndefined } = require('util'); @@ -29,6 +29,7 @@ const server = require('http').Server(app) const port = process.env.PORT || 3000 var client; +var dialogflowFilter = false; var totalMsjs; //MOD by CHV - var vamosA = ""; //MOD by CHV - var newBody; //MOD by CHV - @@ -68,7 +69,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); } @@ -78,11 +79,28 @@ const listenMessage = () => client.on('message', async msg => { */ if (process.env.DATABASE === 'dialogflow') { - if(!message.length) return; - const response = await bothResponse(message); + 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.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, response.trigger); + sendMedia(client, from, response.media); } return } @@ -277,6 +295,28 @@ 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(), puppeteer: { headless: true, args: ['--no-sandbox','--disable-setuid-sandbox'] } @@ -291,6 +331,7 @@ const listenMessage = () => client.on('message', async msg => { client.on('ready', (a) => { connectionReady() listenMessage() + listenMessageFromBot() // socketEvents.sendStatus(client) }); diff --git a/controllers/flows.js b/controllers/flows.js index 42e81a2..043fdc8 100644 --- a/controllers/flows.js +++ b/controllers/flows.js @@ -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, num) => { //MOD by CHV - Agregamos el parametro "num" para recibir el numero desde "app.js" // console.log("GETMESSAGES (flow.js)") @@ -9,9 +9,9 @@ const getMessages = async (message, num) => { //MOD by CHV - Agregamos el parame 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 } @@ -25,5 +25,13 @@ const bothResponse = async (message) => { 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 } \ No newline at end of file +module.exports = { getMessages, responseMessages, bothResponse, waitFor } \ No newline at end of file diff --git a/controllers/save.js b/controllers/save.js index 881cb3d..5dd5cb1 100644 --- a/controllers/save.js +++ b/controllers/save.js @@ -1,23 +1,38 @@ -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! * @param {*} media */ + const saveMedia = (media) => { - const extensionProcess = mimeDb[media.mimetype]; - let ext; - if (!extensionProcess) { - const fileType = media.mimetype.split('/'); - ext = fileType[1].split(';')[0]; - } else { - 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} \ No newline at end of file +const saveMediaToGoogleDrive = async (media) => { + + fileName = saveMedia(media); + filePath = `${__dirname}/../media/${fileName}` + + const googleDriveUrl = await uploadSingleFile(fileName, filePath); + return googleDriveUrl +} + +module.exports = { saveMedia, saveMediaToGoogleDrive } \ No newline at end of file diff --git a/controllers/send.js b/controllers/send.js index 45ca85d..3a757a5 100644 --- a/controllers/send.js +++ b/controllers/send.js @@ -78,6 +78,7 @@ const sendMessageButton = async (client, number = null, text = null, actionButto number = cleanNumber(number) const { title = null, message = null, footer = null, buttons = [] } = actionButtons; let button = new Buttons(remplazos(message, client),[...buttons], remplazos(title, client), remplazos(footer, client)); + await readChat(number, message, actionButtons) client.sendMessage(number, button); console.log(`⚡⚡⚡ Enviando mensajes (botones)....`); // console.log("sendMessageButton."); diff --git a/flow/dialogflow.json b/flow/dialogflow.json new file mode 100644 index 0000000..41111c8 --- /dev/null +++ b/flow/dialogflow.json @@ -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." + ] + } +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 35bab7f..7ef9786 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,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", - "nanoid": "^3.0.0", + "pb-util": "^1.0.3", "qr-image": "^3.2.0", "qrcode-terminal": "^0.12.0", "socket.io": "^4.5.1", @@ -538,9 +539,9 @@ "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" }, "node_modules/@types/node": { - "version": "14.18.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.35.tgz", - "integrity": "sha512-2ATO8pfhG1kDvw4Lc4C0GXIMSQFFJBCo/R1fSgTwmUlq5oy95LXyjDQinsRVgQY6gp6ghh3H91wk9ES5/5C+Tw==" + "version": "14.18.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", + "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==" }, "node_modules/@types/yauzl": { "version": "2.10.0", @@ -2331,6 +2332,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", @@ -3168,17 +3205,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/needle": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", @@ -3476,6 +3502,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", @@ -5009,6 +5040,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", diff --git a/package.json b/package.json index 17d682d..1d70aa0 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,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", - "nanoid": "^3.0.0", + "pb-util": "^1.0.3", "qr-image": "^3.2.0", "qrcode-terminal": "^0.12.0", "socket.io": "^4.5.1", diff --git a/spam.json b/spam.json index ac26bc4..edb4051 100644 --- a/spam.json +++ b/spam.json @@ -1,41 +1,38 @@ [ - { - "numero": "5215554192439" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215527026728" - }, - { - "numero": "5215554192439" - } + { + "numero":"5215554192439" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215527026728" + }, + { + "numero":"5215554192439" + } ] \ No newline at end of file