From f769273974ee06be42375fafd4505386688c81da Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Sun, 20 Feb 2022 14:46:48 -0300 Subject: [PATCH 1/9] :feat: adding dialogflow support in the backend --- backend/package.json | 4 +- backend/src/database/index.ts | 2 + .../20220218095931-create-dialogflow.ts | 44 ++++++++++++ ...20220218095932-add-dialogflow-to-queues.ts | 16 +++++ ...18095933-add-use-dialogflow-to-contacts.ts | 15 ++++ ...220218095934-add-use-queues-to-contacts.ts | 15 ++++ backend/src/libs/wbot.ts | 12 ++++ backend/src/models/Contact.ts | 8 +++ backend/src/models/Dialogflow.ts | 45 ++++++++++++ backend/src/models/Queue.ts | 12 +++- .../CreateSessionDialogflow.ts | 27 +++++++ .../DialogflowServices/QueryDialogflow.ts | 72 +++++++++++++++++++ .../TicketServices/ShowTicketService.ts | 3 +- .../WbotServices/wbotMessageListener.ts | 55 ++++++++++++-- frontend/.docker/add-env-vars.sh | 2 + 15 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 backend/src/database/migrations/20220218095931-create-dialogflow.ts create mode 100644 backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts create mode 100644 backend/src/database/migrations/20220218095933-add-use-dialogflow-to-contacts.ts create mode 100644 backend/src/database/migrations/20220218095934-add-use-queues-to-contacts.ts create mode 100644 backend/src/models/Dialogflow.ts create mode 100644 backend/src/services/DialogflowServices/CreateSessionDialogflow.ts create mode 100644 backend/src/services/DialogflowServices/QueryDialogflow.ts diff --git a/backend/package.json b/backend/package.json index e29e1d8b0..b256c4384 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "tsc", "watch": "tsc -w", - "start": "nodemon dist/server.js", + "start": "nodemon --ignore dialogflow/ dist/server.js", "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts", "pretest": "NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all", "test": "NODE_ENV=test jest", @@ -15,8 +15,10 @@ "author": "", "license": "MIT", "dependencies": { + "@google-cloud/dialogflow": "^4.6.0", "@sentry/node": "^5.29.2", "@types/pino": "^6.3.4", + "actions-on-google": "^3.0.0", "bcryptjs": "^2.4.3", "cookie-parser": "^1.4.5", "cors": "^2.8.5", diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 4c230acd7..c2c0b3b2a 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -10,6 +10,7 @@ import Queue from "../models/Queue"; import WhatsappQueue from "../models/WhatsappQueue"; import UserQueue from "../models/UserQueue"; import QuickAnswer from "../models/QuickAnswer"; +import Dialogflow from "../models/Dialogflow"; // eslint-disable-next-line const dbConfig = require("../config/database"); @@ -25,6 +26,7 @@ const models = [ Whatsapp, ContactCustomField, Setting, + Dialogflow, Queue, WhatsappQueue, UserQueue, diff --git a/backend/src/database/migrations/20220218095931-create-dialogflow.ts b/backend/src/database/migrations/20220218095931-create-dialogflow.ts new file mode 100644 index 000000000..151d1face --- /dev/null +++ b/backend/src/database/migrations/20220218095931-create-dialogflow.ts @@ -0,0 +1,44 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.createTable("Dialogflows", { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + projectName: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + jsonContent: { + type: DataTypes.TEXT, + allowNull: false, + }, + language: { + type: DataTypes.STRING, + allowNull: false, + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false + } + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.dropTable("Dialogflows"); + } +}; diff --git a/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts b/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts new file mode 100644 index 000000000..44ed18170 --- /dev/null +++ b/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts @@ -0,0 +1,16 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Queues", "dialogflowId", { + type: DataTypes.INTEGER, + references: { model: "Dialogflow", key: "id" }, + onUpdate: "CASCADE", + onDelete: "SET NULL" + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Queues", "dialogflowId"); + } +}; diff --git a/backend/src/database/migrations/20220218095933-add-use-dialogflow-to-contacts.ts b/backend/src/database/migrations/20220218095933-add-use-dialogflow-to-contacts.ts new file mode 100644 index 000000000..6231a556e --- /dev/null +++ b/backend/src/database/migrations/20220218095933-add-use-dialogflow-to-contacts.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Contacts", "useDialogflow", { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Contacts", "useDialogflow"); + } +}; diff --git a/backend/src/database/migrations/20220218095934-add-use-queues-to-contacts.ts b/backend/src/database/migrations/20220218095934-add-use-queues-to-contacts.ts new file mode 100644 index 000000000..e29172663 --- /dev/null +++ b/backend/src/database/migrations/20220218095934-add-use-queues-to-contacts.ts @@ -0,0 +1,15 @@ +import { QueryInterface, DataTypes } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.addColumn("Contacts", "useQueues", { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: true + }); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.removeColumn("Contacts", "useQueues"); + } +}; diff --git a/backend/src/libs/wbot.ts b/backend/src/libs/wbot.ts index f58830b4a..67cbdafee 100644 --- a/backend/src/libs/wbot.ts +++ b/backend/src/libs/wbot.ts @@ -129,6 +129,18 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { resolve(wbot); }); + + // wbot.on('message', async msg => { + // wbot.sendPresenceAvailable(); + + // let textoResposta = await executeQueries("whaticket-ctas", msg.from, msg.body, 'pt-BR'); + // if(textoResposta === null || textoResposta === undefined) { + // console.log('Não houve resposta do dialogflow.'); + // return; + // } + + // msg.reply(textoResposta.replace(/\\n/g, '\n')); + // }); } catch (err) { logger.error(err); } diff --git a/backend/src/models/Contact.ts b/backend/src/models/Contact.ts index d7c4c93ca..d2c399e2d 100644 --- a/backend/src/models/Contact.ts +++ b/backend/src/models/Contact.ts @@ -41,6 +41,14 @@ class Contact extends Model { @Column isGroup: boolean; + @Default(true) + @Column + useQueues: boolean; + + @Default(true) + @Column + useDialogflow: boolean; + @CreatedAt createdAt: Date; diff --git a/backend/src/models/Dialogflow.ts b/backend/src/models/Dialogflow.ts new file mode 100644 index 000000000..e33f0a7e0 --- /dev/null +++ b/backend/src/models/Dialogflow.ts @@ -0,0 +1,45 @@ +import { + Table, + Column, + CreatedAt, + UpdatedAt, + Model, + DataType, + PrimaryKey, + HasMany, + AutoIncrement +} from "sequelize-typescript"; +import Queue from "./Queue"; + +@Table +class Dialogflow extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column(DataType.TEXT) + name: string; + + @Column(DataType.TEXT) + projectName: string; + + @Column(DataType.TEXT) + jsonContent: string; + + @Column(DataType.TEXT) + language: string; + + @CreatedAt + @Column(DataType.DATE(6)) + createdAt: Date; + + @UpdatedAt + @Column(DataType.DATE(6)) + updatedAt: Date; + + @HasMany(() => Queue) + queues: Queue[] +} + +export default Dialogflow; diff --git a/backend/src/models/Queue.ts b/backend/src/models/Queue.ts index c5c06d955..e7b056450 100644 --- a/backend/src/models/Queue.ts +++ b/backend/src/models/Queue.ts @@ -8,13 +8,16 @@ import { AutoIncrement, AllowNull, Unique, - BelongsToMany + BelongsToMany, + BelongsTo, + ForeignKey } from "sequelize-typescript"; import User from "./User"; import UserQueue from "./UserQueue"; import Whatsapp from "./Whatsapp"; import WhatsappQueue from "./WhatsappQueue"; +import Dialogflow from "./Dialogflow"; @Table class Queue extends Model { @@ -36,6 +39,13 @@ class Queue extends Model { @Column greetingMessage: string; + @ForeignKey(() => Dialogflow) + @Column + dialogflowId: number; + + @BelongsTo(() => Dialogflow) + dialogflow: Dialogflow; + @CreatedAt createdAt: Date; diff --git a/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts b/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts new file mode 100644 index 000000000..6da194319 --- /dev/null +++ b/backend/src/services/DialogflowServices/CreateSessionDialogflow.ts @@ -0,0 +1,27 @@ +import { SessionsClient } from "@google-cloud/dialogflow"; +import Dialogflow from "../../models/Dialogflow"; +import dir from 'path'; +import fs from 'fs'; +import os from 'os'; +import { logger } from "../../utils/logger"; + +const sessions : Map = new Map(); + +const createDialogflowSession = async (model: Dialogflow) : Promise => { + if(sessions.has(model.id)) { + return sessions.get(model.id); + } + + const keyFilename = dir.join(os.tmpdir(), `whaticket_${model.id}.json`); + + logger.info(`Openig new dialogflow session #${model.projectName} in '${keyFilename}'`) + + await fs.writeFileSync(keyFilename, model.jsonContent); + const session = new SessionsClient({ keyFilename }); + + sessions.set(model.id, session); + + return session; +} + +export { createDialogflowSession }; \ No newline at end of file diff --git a/backend/src/services/DialogflowServices/QueryDialogflow.ts b/backend/src/services/DialogflowServices/QueryDialogflow.ts new file mode 100644 index 000000000..5678e9c6f --- /dev/null +++ b/backend/src/services/DialogflowServices/QueryDialogflow.ts @@ -0,0 +1,72 @@ +import * as Sentry from "@sentry/node"; +import { SessionsClient } from "@google-cloud/dialogflow"; +import { logger } from "../../utils/logger"; + +function isBlank(str:string | undefined | null) { + return (!str || /^\s*$/.test(str)); +} + +async function detectIntent( + sessionClient:SessionsClient, + projectId:string, + sessionId:string, + query:string, + languageCode:string +) { + const sessionPath = sessionClient.projectAgentSessionPath( + projectId, + sessionId + ); + + const request = { + session: sessionPath, + queryInput: { + text: { + text: query, + languageCode: languageCode, + }, + }, + }; + + const responses = await sessionClient.detectIntent(request); + return responses[0]; +} + +async function queryDialogFlow( + sessionClient:SessionsClient, + projectId:string, + sessionId:string, + query:string, + languageCode:string +) : Promise { + let intentResponse; + + try { + console.log(`Dialoflow Question: '${query}'`); + + intentResponse = await detectIntent( + sessionClient, + projectId, + sessionId, + query, + languageCode + ); + + const fulfillmentText = intentResponse?.queryResult?.fulfillmentText; + + if (isBlank(fulfillmentText)) { + console.log('No defined answer in Dialogflow'); + return null; + } else { + console.log(`Dialoflow answer: '${fulfillmentText}'`); + return `${fulfillmentText}` + } + } catch (error) { + Sentry.captureException(error); + logger.error(`Error handling whatsapp message: Err: ${error}`); + } + + return null; +} + +export {queryDialogFlow} \ No newline at end of file diff --git a/backend/src/services/TicketServices/ShowTicketService.ts b/backend/src/services/TicketServices/ShowTicketService.ts index 5efab0cab..bf7910b0d 100644 --- a/backend/src/services/TicketServices/ShowTicketService.ts +++ b/backend/src/services/TicketServices/ShowTicketService.ts @@ -21,7 +21,8 @@ const ShowTicketService = async (id: string | number): Promise => { { model: Queue, as: "queue", - attributes: ["id", "name", "color"] + attributes: ["id", "name", "color"], + include: ["dialogflow"] } ] }); diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index 0345c210f..e150871e6 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -23,8 +23,9 @@ import ShowWhatsAppService from "../WhatsappService/ShowWhatsAppService"; import { debounce } from "../../helpers/Debounce"; import UpdateTicketService from "../TicketServices/UpdateTicketService"; import CreateContactService from "../ContactServices/CreateContactService"; -import GetContactService from "../ContactServices/GetContactService"; import formatBody from "../../helpers/Mustache"; +import { queryDialogFlow } from "../DialogflowServices/QueryDialogflow"; +import { createDialogflowSession } from "../DialogflowServices/CreateSessionDialogflow"; interface Session extends Client { id?: number; @@ -163,6 +164,10 @@ const verifyQueue = async ( return; } + if (!contact.useQueues) { + return; + } + const selectedOption = msg.body; const choosenQueue = queues[+selectedOption - 1]; @@ -173,11 +178,11 @@ const verifyQueue = async ( ticketId: ticket.id }); - const body = formatBody(`\u200e${choosenQueue.greetingMessage}`, contact); - - const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); - - await verifyMessage(sentMessage, ticket, contact); + if( choosenQueue.greetingMessage ) { + let body = formatBody(`\u200e${choosenQueue.greetingMessage}`, contact); + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + await verifyMessage(sentMessage, ticket, contact); + } } else { let options = ""; @@ -203,6 +208,35 @@ const verifyQueue = async ( } }; +const sendDialogflowAwswer = async ( + wbot: Session, + ticket:Ticket, + msg:WbotMessage, + contact: Contact +) => { + const session = await createDialogflowSession(ticket.queue.dialogflow); + if(session === undefined) { + return; + } + + let dialogFlowReply = await queryDialogFlow( + session, + ticket.queue.dialogflow.projectName, + msg.from, + msg.body, + ticket.queue.dialogflow.language + ); + if(dialogFlowReply === null) { + return; + } + + const body = dialogFlowReply.replace(/\\n/g, '\n'); + const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); + await verifyMessage(sentMessage, ticket, contact); +} + + + const isValidMsg = (msg: WbotMessage): boolean => { if (msg.from === "status@broadcast") return false; if ( @@ -299,6 +333,15 @@ const handleMessage = async ( await verifyQueue(wbot, msg, ticket, contact); } + if( + !msg.fromMe && + ticket.queue && + ticket.queue.dialogflow && + contact.useDialogflow + ) { + await sendDialogflowAwswer(wbot, ticket, msg, contact); + } + if (msg.type === "vcard") { try { const array = msg.body.split("\n"); diff --git a/frontend/.docker/add-env-vars.sh b/frontend/.docker/add-env-vars.sh index 4449379de..5f3fff384 100644 --- a/frontend/.docker/add-env-vars.sh +++ b/frontend/.docker/add-env-vars.sh @@ -1,3 +1,5 @@ +#!/bin/sh + _writeFrontendEnvVars() { ENV_JSON="$(jq --compact-output --null-input 'env | with_entries(select(.key | startswith("REACT_APP_")))')" ENV_JSON_ESCAPED="$(printf "%s" "${ENV_JSON}" | sed -e 's/[\&/]/\\&/g')" From 09e815f202b725fdd8c9ed7385e3d26d2913c556 Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Sun, 20 Feb 2022 15:39:36 -0300 Subject: [PATCH 2/9] :fix: Fixing table Dialogflow name --- .../20220218095932-add-dialogflow-to-queues.ts | 2 +- docker-compose.phpmyadmin.yaml | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 docker-compose.phpmyadmin.yaml diff --git a/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts b/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts index 44ed18170..aa6d9ebd9 100644 --- a/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts +++ b/backend/src/database/migrations/20220218095932-add-dialogflow-to-queues.ts @@ -4,7 +4,7 @@ module.exports = { up: (queryInterface: QueryInterface) => { return queryInterface.addColumn("Queues", "dialogflowId", { type: DataTypes.INTEGER, - references: { model: "Dialogflow", key: "id" }, + references: { model: "Dialogflows", key: "id" }, onUpdate: "CASCADE", onDelete: "SET NULL" }); diff --git a/docker-compose.phpmyadmin.yaml b/docker-compose.phpmyadmin.yaml new file mode 100644 index 000000000..2ee6ec382 --- /dev/null +++ b/docker-compose.phpmyadmin.yaml @@ -0,0 +1,15 @@ +version: '3' + +networks: + whaticket: + +services: + + phpmyadmin: + image: phpmyadmin/phpmyadmin:latest + environment: + - PMA_HOSTS=mysql + ports: + - ${PMA_PORT:-9000}:80 + networks: + - whaticket \ No newline at end of file From 4ac88ce77f870c086d3b19e144bcbf44f59dbc7f Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Mon, 21 Feb 2022 08:10:34 -0300 Subject: [PATCH 3/9] :feat: Adding routes in backend to enable/disable `useQueue` in `useDialogflow` --- .env.example | 5 ++- backend/src/controllers/ContactController.ts | 36 +++++++++++++++++++ backend/src/routes/contactRoutes.ts | 4 +++ .../ToggleUseDialogflowContactService.ts | 34 ++++++++++++++++++ .../ToggleUseQueuesContactService.ts | 34 ++++++++++++++++++ .../ContactServices/UpdateContactService.ts | 4 +-- .../TicketServices/ShowTicketService.ts | 2 +- 7 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts create mode 100644 backend/src/services/ContactServices/ToggleUseQueuesContactService.ts diff --git a/.env.example b/.env.example index 033e99fd1..375088ad3 100644 --- a/.env.example +++ b/.env.example @@ -21,4 +21,7 @@ FRONTEND_SERVER_NAME=myapp.mydomain.com FRONTEND_URL=https://myapp.mydomain.com # BROWSERLESS -MAX_CONCURRENT_SESSIONS= \ No newline at end of file +MAX_CONCURRENT_SESSIONS= + +# PHPMYADMIN +PMA_PORT= \ No newline at end of file diff --git a/backend/src/controllers/ContactController.ts b/backend/src/controllers/ContactController.ts index 767863a76..2de6d5510 100644 --- a/backend/src/controllers/ContactController.ts +++ b/backend/src/controllers/ContactController.ts @@ -13,6 +13,8 @@ import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact"; import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import AppError from "../errors/AppError"; import GetContactService from "../services/ContactServices/GetContactService"; +import ToggleUseQueuesContactService from "../services/ContactServices/ToggleUseQueuesContactService"; +import ToggleUseDialogflowContactService from "../services/ContactServices/ToggleUseDialogflowContactService"; type IndexQuery = { searchParam: string; @@ -144,6 +146,40 @@ export const update = async ( return res.status(200).json(contact); }; +export const toggleUseQueue = async ( + req: Request, + res: Response +): Promise => { + const { contactId } = req.params; + + const contact = await ToggleUseQueuesContactService({ contactId }); + + const io = getIO(); + io.emit("contact", { + action: "update", + contact + }); + + return res.status(200).json(contact); +}; + +export const toggleUseDialogflow = async ( + req: Request, + res: Response +): Promise => { + const { contactId } = req.params; + + const contact = await ToggleUseDialogflowContactService({ contactId }); + + const io = getIO(); + io.emit("contact", { + action: "update", + contact + }); + + return res.status(200).json(contact); +}; + export const remove = async ( req: Request, res: Response diff --git a/backend/src/routes/contactRoutes.ts b/backend/src/routes/contactRoutes.ts index 1e8ff2b4e..223cbe119 100644 --- a/backend/src/routes/contactRoutes.ts +++ b/backend/src/routes/contactRoutes.ts @@ -22,6 +22,10 @@ contactRoutes.post("/contact", isAuth, ContactController.getContact); contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); +contactRoutes.put("/contacts/toggleUseQueue/:contactId", isAuth, ContactController.toggleUseQueue); + +contactRoutes.put("/contacts/toggleUseDialogflow/:contactId", isAuth, ContactController.toggleUseDialogflow); + contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); export default contactRoutes; diff --git a/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts b/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts new file mode 100644 index 000000000..0034617dd --- /dev/null +++ b/backend/src/services/ContactServices/ToggleUseDialogflowContactService.ts @@ -0,0 +1,34 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; + +interface Request { + contactId: string; +} + +const ToggleUseDialogflowContactService = async ({ + contactId +}: Request): Promise => { + const contact = await Contact.findOne({ + where: { id: contactId }, + attributes: ["id", "useDialogflow"] + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + const useDialogflow = contact.useDialogflow ? false : true; + + await contact.update({ + useDialogflow + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], + include: ["extraInfo"] + }); + + return contact; +}; + +export default ToggleUseDialogflowContactService; diff --git a/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts b/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts new file mode 100644 index 000000000..b7d512743 --- /dev/null +++ b/backend/src/services/ContactServices/ToggleUseQueuesContactService.ts @@ -0,0 +1,34 @@ +import AppError from "../../errors/AppError"; +import Contact from "../../models/Contact"; + +interface Request { + contactId: string; +} + +const ToggleUseQueuesContactService = async ({ + contactId +}: Request): Promise => { + const contact = await Contact.findOne({ + where: { id: contactId }, + attributes: ["id", "useQueues"] + }); + + if (!contact) { + throw new AppError("ERR_NO_CONTACT_FOUND", 404); + } + + const useQueues = contact.useQueues ? false : true; + + await contact.update({ + useQueues + }); + + await contact.reload({ + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], + include: ["extraInfo"] + }); + + return contact; +}; + +export default ToggleUseQueuesContactService; diff --git a/backend/src/services/ContactServices/UpdateContactService.ts b/backend/src/services/ContactServices/UpdateContactService.ts index 82117665e..736b45aa8 100644 --- a/backend/src/services/ContactServices/UpdateContactService.ts +++ b/backend/src/services/ContactServices/UpdateContactService.ts @@ -27,7 +27,7 @@ const UpdateContactService = async ({ const contact = await Contact.findOne({ where: { id: contactId }, - attributes: ["id", "name", "number", "email", "profilePicUrl"], + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], include: ["extraInfo"] }); @@ -60,7 +60,7 @@ const UpdateContactService = async ({ }); await contact.reload({ - attributes: ["id", "name", "number", "email", "profilePicUrl"], + attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"], include: ["extraInfo"] }); diff --git a/backend/src/services/TicketServices/ShowTicketService.ts b/backend/src/services/TicketServices/ShowTicketService.ts index bf7910b0d..c6bf50fa0 100644 --- a/backend/src/services/TicketServices/ShowTicketService.ts +++ b/backend/src/services/TicketServices/ShowTicketService.ts @@ -10,7 +10,7 @@ const ShowTicketService = async (id: string | number): Promise => { { model: Contact, as: "contact", - attributes: ["id", "name", "number", "profilePicUrl"], + attributes: ["id", "name", "number", "profilePicUrl", "useDialogflow", "useQueues"], include: ["extraInfo"] }, { From 7c0364c5550b56040ce44bf3ca7f0d33ba12f0bb Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Mon, 21 Feb 2022 08:51:58 -0300 Subject: [PATCH 4/9] :feat: Adding switch in ticket detail to enable/disable dialogflow for contact --- .../components/TicketActionButtons/index.js | 25 ++++++++++++++++++- frontend/src/translate/languages/en.js | 2 ++ frontend/src/translate/languages/es.js | 2 ++ frontend/src/translate/languages/pt.js | 2 ++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TicketActionButtons/index.js b/frontend/src/components/TicketActionButtons/index.js index 9cf58dea3..fdec7f4fd 100644 --- a/frontend/src/components/TicketActionButtons/index.js +++ b/frontend/src/components/TicketActionButtons/index.js @@ -2,7 +2,7 @@ import React, { useContext, useState } from "react"; import { useHistory } from "react-router-dom"; import { makeStyles } from "@material-ui/core/styles"; -import { IconButton } from "@material-ui/core"; +import { FormControlLabel, IconButton, Switch } from "@material-ui/core"; import { MoreVert, Replay } from "@material-ui/icons"; import { i18n } from "../../translate/i18n"; @@ -29,6 +29,7 @@ const TicketActionButtons = ({ ticket }) => { const history = useHistory(); const [anchorEl, setAnchorEl] = useState(null); const [loading, setLoading] = useState(false); + const [useDialogflow, setUseDialogflow] = useState(ticket.contact.useDialogflow); const ticketOptionsMenuOpen = Boolean(anchorEl); const { user } = useContext(AuthContext); @@ -60,8 +61,30 @@ const TicketActionButtons = ({ ticket }) => { } }; + const handleContactToggleUseDialogflow = async (e, status, userId) => { + setLoading(true); + try { + const contact = await api.put(`/contacts/toggleUseDialogflow/${ticket.contact.id}`); + setUseDialogflow(contact.data.useDialogflow); + setLoading(false); + } catch (err) { + setLoading(false); + toastError(err); + } + }; + return (
+ handleContactToggleUseDialogflow(e)} + /> + } + label={i18n.t("messagesList.header.buttons.dialogflow")} + /> {ticket.status === "closed" && ( Date: Tue, 22 Feb 2022 01:22:26 -0300 Subject: [PATCH 5/9] :sparkles: Send delayed multi-line dialogflow response --- .../WbotServices/wbotMessageListener.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index e150871e6..eade66000 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -7,7 +7,8 @@ import { Contact as WbotContact, Message as WbotMessage, MessageAck, - Client + Client, + Chat } from "whatsapp-web.js"; import Contact from "../../models/Contact"; @@ -212,13 +213,16 @@ const sendDialogflowAwswer = async ( wbot: Session, ticket:Ticket, msg:WbotMessage, - contact: Contact + contact: Contact, + chat:Chat ) => { const session = await createDialogflowSession(ticket.queue.dialogflow); if(session === undefined) { return; } + wbot.sendPresenceAvailable(); + let dialogFlowReply = await queryDialogFlow( session, ticket.queue.dialogflow.projectName, @@ -230,12 +234,19 @@ const sendDialogflowAwswer = async ( return; } - const body = dialogFlowReply.replace(/\\n/g, '\n'); - const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body); - await verifyMessage(sentMessage, ticket, contact); -} + chat.sendStateTyping(); + await new Promise(f => setTimeout(f, 1000)); + const body = dialogFlowReply.replace(/\\n/g, '\n'); + const linesOfBody = body.split('\n'); + + for(let i=0;i setTimeout(f, 1000)); + } +} const isValidMsg = (msg: WbotMessage): boolean => { if (msg.from === "status@broadcast") return false; @@ -339,7 +350,7 @@ const handleMessage = async ( ticket.queue.dialogflow && contact.useDialogflow ) { - await sendDialogflowAwswer(wbot, ticket, msg, contact); + await sendDialogflowAwswer(wbot, ticket, msg, contact, chat); } if (msg.type === "vcard") { From 6fad165d3c28c9ddcd7a028c4f47d2cc13c5e812 Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Tue, 22 Feb 2022 01:30:37 -0300 Subject: [PATCH 6/9] :hammer: Removing Code Smells --- backend/src/libs/wbot.ts | 12 ------------ .../services/DialogflowServices/QueryDialogflow.ts | 2 +- .../src/services/WbotServices/wbotMessageListener.ts | 4 ++-- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/backend/src/libs/wbot.ts b/backend/src/libs/wbot.ts index 67cbdafee..f58830b4a 100644 --- a/backend/src/libs/wbot.ts +++ b/backend/src/libs/wbot.ts @@ -129,18 +129,6 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { resolve(wbot); }); - - // wbot.on('message', async msg => { - // wbot.sendPresenceAvailable(); - - // let textoResposta = await executeQueries("whaticket-ctas", msg.from, msg.body, 'pt-BR'); - // if(textoResposta === null || textoResposta === undefined) { - // console.log('Não houve resposta do dialogflow.'); - // return; - // } - - // msg.reply(textoResposta.replace(/\\n/g, '\n')); - // }); } catch (err) { logger.error(err); } diff --git a/backend/src/services/DialogflowServices/QueryDialogflow.ts b/backend/src/services/DialogflowServices/QueryDialogflow.ts index 5678e9c6f..e5fe40319 100644 --- a/backend/src/services/DialogflowServices/QueryDialogflow.ts +++ b/backend/src/services/DialogflowServices/QueryDialogflow.ts @@ -38,7 +38,7 @@ async function queryDialogFlow( sessionId:string, query:string, languageCode:string -) : Promise { +) : Promise { let intentResponse; try { diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index eade66000..714cf19ba 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -241,8 +241,8 @@ const sendDialogflowAwswer = async ( const body = dialogFlowReply.replace(/\\n/g, '\n'); const linesOfBody = body.split('\n'); - for(let i=0;i setTimeout(f, 1000)); } From 0fd973fe07d7766a4adf2bc4cd846edc461c151f Mon Sep 17 00:00:00 2001 From: Ricardo Paes Date: Tue, 1 Mar 2022 08:14:48 -0300 Subject: [PATCH 7/9] :sparkles: Add button in Frontend to enable/disable Contact Queues --- backend/src/routes/contactRoutes.ts | 2 +- .../src/components/TicketOptionsMenu/index.js | 19 +++++++++++++++++++ frontend/src/translate/languages/en.js | 1 + frontend/src/translate/languages/es.js | 1 + frontend/src/translate/languages/pt.js | 1 + 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/contactRoutes.ts b/backend/src/routes/contactRoutes.ts index 223cbe119..9b4e887f6 100644 --- a/backend/src/routes/contactRoutes.ts +++ b/backend/src/routes/contactRoutes.ts @@ -22,7 +22,7 @@ contactRoutes.post("/contact", isAuth, ContactController.getContact); contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); -contactRoutes.put("/contacts/toggleUseQueue/:contactId", isAuth, ContactController.toggleUseQueue); +contactRoutes.put("/contacts/toggleUseQueues/:contactId", isAuth, ContactController.toggleUseQueue); contactRoutes.put("/contacts/toggleUseDialogflow/:contactId", isAuth, ContactController.toggleUseDialogflow); diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 16591a262..08ab73504 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -10,10 +10,12 @@ import TransferTicketModal from "../TransferTicketModal"; import toastError from "../../errors/toastError"; import { Can } from "../Can"; import { AuthContext } from "../../context/Auth/AuthContext"; +import { Switch } from "@material-ui/core"; const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { const [confirmationOpen, setConfirmationOpen] = useState(false); const [transferTicketModalOpen, setTransferTicketModalOpen] = useState(false); + const [useQueues, setUseQueues] = useState(ticket.contact.useQueues); const isMounted = useRef(true); const { user } = useContext(AuthContext); @@ -47,6 +49,15 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { } }; + const handleContactToggleUseQueues = async () => { + try { + const contact = await api.put(`/contacts/toggleUseQueues/${ticket.contact.id}`); + setUseQueues(contact.data.useQueues); + } catch (err) { + toastError(err); + } + }; + return ( <> { {i18n.t("ticketOptionsMenu.transfer")} + {ticket.queue && + handleContactToggleUseQueues(e)} + /> + {i18n.t("ticketOptionsMenu.useQueues")} + } Date: Tue, 1 Mar 2022 08:15:29 -0300 Subject: [PATCH 8/9] :sparkles: Show the dialogflow key only when it is configured for that queue --- .../components/TicketActionButtons/index.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/TicketActionButtons/index.js b/frontend/src/components/TicketActionButtons/index.js index fdec7f4fd..6c9baa796 100644 --- a/frontend/src/components/TicketActionButtons/index.js +++ b/frontend/src/components/TicketActionButtons/index.js @@ -61,7 +61,7 @@ const TicketActionButtons = ({ ticket }) => { } }; - const handleContactToggleUseDialogflow = async (e, status, userId) => { + const handleContactToggleUseDialogflow = async () => { setLoading(true); try { const contact = await api.put(`/contacts/toggleUseDialogflow/${ticket.contact.id}`); @@ -75,16 +75,18 @@ const TicketActionButtons = ({ ticket }) => { return (
- handleContactToggleUseDialogflow(e)} - /> - } - label={i18n.t("messagesList.header.buttons.dialogflow")} - /> + {ticket?.queue?.dialogflow && + handleContactToggleUseDialogflow(e)} + /> + } + label={i18n.t("messagesList.header.buttons.dialogflow")} + /> + } {ticket.status === "closed" && ( Date: Tue, 1 Mar 2022 08:28:44 -0300 Subject: [PATCH 9/9] :bug: Removing bugs --- frontend/src/components/TicketActionButtons/index.js | 2 +- frontend/src/components/TicketOptionsMenu/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/TicketActionButtons/index.js b/frontend/src/components/TicketActionButtons/index.js index 6c9baa796..b553ccb5f 100644 --- a/frontend/src/components/TicketActionButtons/index.js +++ b/frontend/src/components/TicketActionButtons/index.js @@ -81,7 +81,7 @@ const TicketActionButtons = ({ ticket }) => { handleContactToggleUseDialogflow(e)} + onChange={() => handleContactToggleUseDialogflow()} /> } label={i18n.t("messagesList.header.buttons.dialogflow")} diff --git a/frontend/src/components/TicketOptionsMenu/index.js b/frontend/src/components/TicketOptionsMenu/index.js index 08ab73504..b9d546f00 100644 --- a/frontend/src/components/TicketOptionsMenu/index.js +++ b/frontend/src/components/TicketOptionsMenu/index.js @@ -83,7 +83,7 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { handleContactToggleUseQueues(e)} + onChange={() => handleContactToggleUseQueues()} /> {i18n.t("ticketOptionsMenu.useQueues")} }